import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
import { join, resolve, dirname } from 'node:path';
import { readdirSync, readFileSync, existsSync, writeFileSync, mkdirSync, rmSync, statSync } from 'node:fs';
import { gzipSync } from 'node:zlib';
import { homedir } from 'node:os';
import { execSync } from 'node:child_process';
import { createLogger, generateId, userId as genUserId, kebab, saveConfig, getTextContent, stripInternalBlocks, extractThinkBlocks, APP_VERSION, checkForUpdate, buildManifest, manifestFilename, CHANNEL_CONTEXT_MESSAGES, type TaskStatus, type TaskPriority, type TaskSortField, type SortOrder, type PackageType, type RequirementStatus } from '@markus/shared';
import {
  GatewayError,
  WorkflowEngine,
  createDefaultTeamTemplates,
  createDefaultTemplateRegistry,
  generateHandbook,
  GatewaySyncHandler,
  type TeamTemplateRegistry,
  type AgentToolHandler,
  type ExternalAgentGateway,
  type LLMRouter,
  type ReviewService,
  type SkillRegistry,
  type TemplateRegistry,
  type WorkflowExecutor,
  type WorkflowDefinition,
  type SyncRequest,
  type HandbookColleague,
  type HandbookProject,
  discoverSkillsInDir,
  WELL_KNOWN_SKILL_DIRS,
  type AgentManager,
} from '@markus/core';
import type { ChannelMsg } from '@markus/storage';
import type { OrganizationService } from './org-service.js';
import { BuilderService } from './builder-service.js';
import type { TaskService } from './task-service.js';
import type { HITLService } from './hitl-service.js';
import type { BillingService } from './billing-service.js';
import type { AuditService, AuditEventType } from './audit-service.js';
import type { StorageBridge } from './storage-bridge.js';
import type { ProjectService } from './project-service.js';
import type { ReportService } from './report-service.js';
import type { KnowledgeService } from './knowledge-service.js';
import type { DeliverableService } from './deliverable-service.js';
import type { RequirementService } from './requirement-service.js';
import { WSBroadcaster } from './ws-server.js';
import { SSEHandler } from './sse-handler.js';
import { installSkill } from './skill-service.js';
import type { LocalFileStorageProvider } from './file-storage-provider.js';

const log = createLogger('api-server');

// ── Auth helpers ──────────────────────────────────────────────────────────────
// Simple JWT-lite using HMAC-SHA256 (no external deps required)
async function signToken(payload: Record<string, unknown>, secret: string): Promise<string> {
  const header = btoa(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
  const body = btoa(JSON.stringify(payload));
  const data = `${header}.${body}`;
  const key = await crypto.subtle.importKey(
    'raw',
    new TextEncoder().encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign']
  );
  const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(data));
  const sigB64 = btoa(String.fromCharCode(...new Uint8Array(sig)))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=/g, '');
  return `${data}.${sigB64}`;
}

async function verifyToken(token: string, secret: string): Promise<Record<string, unknown> | null> {
  try {
    const parts = token.split('.');
    if (parts.length !== 3) return null;
    const data = `${parts[0]}.${parts[1]}`;
    const key = await crypto.subtle.importKey(
      'raw',
      new TextEncoder().encode(secret),
      { name: 'HMAC', hash: 'SHA-256' },
      false,
      ['verify']
    );
    const sigBytes = Uint8Array.from(atob(parts[2]!.replace(/-/g, '+').replace(/_/g, '/')), c =>
      c.charCodeAt(0)
    );
    const valid = await crypto.subtle.verify('HMAC', key, sigBytes, new TextEncoder().encode(data));
    if (!valid) return null;
    const payload = JSON.parse(atob(parts[1]!)) as Record<string, unknown>;
    if (payload['exp'] && (payload['exp'] as number) < Date.now() / 1000) return null;
    return payload;
  } catch {
    return null;
  }
}

const PBKDF2_ITERATIONS = 10000;

async function hashPassword(password: string): Promise<string> {
  // Format: pbkdf2:<iterations>:<saltHex>:<hashHex>
  const salt = crypto.getRandomValues(new Uint8Array(16));
  const saltHex = Array.from(salt)
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
  const key = await crypto.subtle.importKey(
    'raw',
    new TextEncoder().encode(password),
    'PBKDF2',
    false,
    ['deriveBits']
  );
  const bits = await crypto.subtle.deriveBits(
    { name: 'PBKDF2', salt, iterations: PBKDF2_ITERATIONS, hash: 'SHA-256' },
    key,
    256
  );
  const hashHex = Array.from(new Uint8Array(bits))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
  return `pbkdf2:${PBKDF2_ITERATIONS}:${saltHex}:${hashHex}`;
}

async function verifyPassword(password: string, stored: string): Promise<boolean> {
  const parts = stored.split(':');
  // Support old format (3 parts) and new format (4 parts with iterations)
  if (parts[0] !== 'pbkdf2') return false;
  let iterations: number;
  let saltHex: string;
  let expectedHash: string;
  if (parts.length === 4) {
    iterations = parseInt(parts[1]!, 10);
    saltHex = parts[2]!;
    expectedHash = parts[3]!;
  } else if (parts.length === 3) {
    iterations = 100000; // legacy
    saltHex = parts[1]!;
    expectedHash = parts[2]!;
  } else {
    return false;
  }
  const salt = Uint8Array.from((saltHex.match(/.{2}/g) ?? []).map(b => parseInt(b, 16)));
  const key = await crypto.subtle.importKey(
    'raw',
    new TextEncoder().encode(password),
    'PBKDF2',
    false,
    ['deriveBits']
  );
  const bits = await crypto.subtle.deriveBits(
    { name: 'PBKDF2', salt, iterations, hash: 'SHA-256' },
    key,
    256
  );
  const hashHex = Array.from(new Uint8Array(bits))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
  return hashHex === expectedHash;
}

function generateInviteToken(): string {
  const bytes = crypto.getRandomValues(new Uint8Array(32));
  return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
}

function parseCookies(cookieHeader: string | undefined): Record<string, string> {
  if (!cookieHeader) return {};
  const result: Record<string, string> = {};
  for (const part of cookieHeader.split(';')) {
    const eqIdx = part.indexOf('=');
    if (eqIdx === -1) continue;
    const key = decodeURIComponent(part.slice(0, eqIdx).trim());
    const val = decodeURIComponent(part.slice(eqIdx + 1).trim());
    if (key) result[key] = val;
  }
  return result;
}

export class APIServer {
  private server?: ReturnType<typeof createServer>;
  private ws: WSBroadcaster;
  private skillRegistry?: SkillRegistry;
  private hitlService?: HITLService;
  private billingService?: BillingService;
  private auditService?: AuditService;
  private storage?: StorageBridge;
  private llmRouter?: LLMRouter;
  private markusConfigPath?: string;
  private hubUrl = 'https://markus.global';
  private webUiDir?: string;
  private gateway?: ExternalAgentGateway;
  private gatewaySecret?: string;
  private syncHandler?: GatewaySyncHandler;
  private gatewayMessageQueue = new Map<string, Array<{ id: string; from: string; fromName: string; content: string; timestamp: string }>>();
  private reviewService?: ReviewService;
  private registryCache?: Map<string, { data: unknown; ts: number }>;
  private templateRegistry?: TemplateRegistry;
  private builderService?: BuilderService;
  private workflowEngine?: WorkflowEngine;
  private teamTemplateRegistry: TeamTemplateRegistry;
  private fileStorage?: LocalFileStorageProvider;
  private remoteAgent?: { getStatus(): unknown; start(): Promise<void>; stop(): Promise<void>; onStatus(cb: (s: unknown) => void): () => void };
  private remoteAgentFactory?: () => Promise<{ getStatus(): unknown; start(): Promise<void>; stop(): Promise<void>; onStatus(cb: (s: unknown) => void): () => void } | null>;
  // Custom group chats are now persisted in SQLite via storage.groupChatRepo
  constructor(
    private orgService: OrganizationService,
    private taskService: TaskService,
    private port: number = 8056
  ) {
    this.ws = new WSBroadcaster();
    this.teamTemplateRegistry = createDefaultTeamTemplates();
    this.templateRegistry = createDefaultTemplateRegistry();
    // Propagate template registry to AgentManager so createAgentFromTemplate works
    const am = this.orgService.getAgentManager();
    if (this.templateRegistry && !am.getTemplateRegistry()) {
      am.setTemplateRegistry(this.templateRegistry);
    }

    // Wire up group chat handlers for agent communication tools
    am.setGroupChatHandlers({
      sendGroupMessage: async (
        channelKey: string,
        message: string,
        senderId: string,
        senderName: string,
        replyToId?: string
      ) => {
        let cleanText = stripInternalBlocks(message);

        // Reject NO_RESPONSE variants — agent should simply not call this tool.
        const NO_RESPONSE_RE = /\[NO[_\s-]?RESPONSE[^\]]*\]/i;
        const NO_RESPONSE_CREATIVE_RE = /^\[(?:context check|no response|silent|listening|observing|monitoring|watching|noting|acknowledged?)[^\]]*\]$/i;
        if (NO_RESPONSE_RE.test(cleanText.trim()) || NO_RESPONSE_CREATIVE_RE.test(cleanText.trim())) {
          throw new Error(
            'Message blocked: you sent a [NO_RESPONSE] variant via agent_send_group_message. ' +
            'If you have nothing to say, simply do NOT call this tool. ' +
            '[NO_RESPONSE] is only valid as a direct reply, not as a tool call argument.'
          );
        }

        // Reject raw tool commands and slash commands — agent should use the actual tool.
        const TOOL_CMD_RE = /^\s*\/?(?:recall_context|memory_search|memory_save|task_get|task_list|task_comment|requirement_get|requirement_comment|file_read|agent_send_message|agent_send_group_message|check_mailbox|update_working_memory|clear_working_memory|defer_mailbox_item|drop_mailbox_item|prioritize_mailbox_item|notify_user|recall_activity)\b/i;
        const SLASH_CMD_RE = /^\s*\/(?:history|help|status|list|search|get|set|info|ping|who|whois|me|join|leave|invite|kick|ban|mute|unmute|clear|purge|poll|remind|note|todo|roll|flip|ask)\b/i;
        const blockedLines = cleanText.split('\n').filter(line => TOOL_CMD_RE.test(line) || SLASH_CMD_RE.test(line));
        if (blockedLines.length > 0) {
          cleanText = cleanText.split('\n')
            .filter(line => !TOOL_CMD_RE.test(line) && !SLASH_CMD_RE.test(line))
            .join('\n').trim();
          if (!cleanText) {
            throw new Error(
              `Message blocked: "${blockedLines[0].trim()}" is not a valid chat message. ` +
              'Slash commands like /history do not exist. ' +
              'To fetch channel history, use the recall_context tool with scope="channel". ' +
              'To interact with tasks, use task_get/task_list. ' +
              'Do NOT send tool names or slash commands as chat messages.'
            );
          }
        }

        const orgId = 'default';

        // Persist agent message
        let persistedMsgId: string | undefined;
        if (this.storage) {
          const saved = await this.storage.channelMessageRepo.append({
            orgId,
            channel: channelKey,
            senderId,
            senderType: 'agent',
            senderName,
            text: cleanText,
            replyToId,
          });
          persistedMsgId = saved.id;
          this.ws.broadcastUnreadUpdate(`channel:${channelKey}`, saved.id);
        }

        // Send to frontend via WebSocket (scoped to channel members)
        const channelEvent = {
          type: 'chat:message' as const,
          payload: {
            channel: channelKey,
            senderId,
            senderType: 'agent',
            senderName,
            text: cleanText,
          },
          timestamp: new Date().toISOString(),
        };
        if (channelKey.startsWith('dm:') || channelKey.startsWith('notes:')) {
          const participants = channelKey.startsWith('notes:')
            ? [channelKey.slice(6)]
            : channelKey.slice(3).split(':');
          this.ws.sendToUsers(participants, channelEvent);
        } else {
          const humanIds = this.resolveChannelHumanIds(channelKey);
          if (humanIds.length > 0) this.ws.sendToUsers(humanIds, channelEvent);
          else this.ws.broadcast(channelEvent);
        }

        // Resolve all agent members in this group channel
        let allAgentIds: string[] = [];
        if (channelKey.startsWith('group:custom:') && this.storage?.groupChatRepo) {
          allAgentIds = this.storage.groupChatRepo.getAgentMemberIds(channelKey);
        } else if (channelKey.startsWith('group:')) {
          const teamId = channelKey.replace(/^group:/, '');
          const team = this.orgService.getTeam(teamId);
          allAgentIds = team?.memberAgentIds ?? [];
        }

        // Notify peer agents in the group
        const peerAgentIds = allAgentIds.filter(id => id !== senderId);
        if (peerAgentIds.length > 0) {
          const agentManager = this.orgService.getAgentManager();
          const nameMap = this.buildAgentNameMap(allAgentIds, agentManager);
          const mentionedNames = this.parseAgentMentions(cleanText, [...nameMap.keys()]);
          const filteredMentions = mentionedNames.filter(n => {
            const id = nameMap.get(n);
            return id && id !== senderId;
          });

          if (filteredMentions.length > 0) {
            // Targeted: trigger A2A chain for @mentioned agents
            let channelContext: Array<{ role: string; content: string }> = [];
            if (this.storage) {
              try {
                const recent = await this.storage.channelMessageRepo.getMessages(channelKey, CHANNEL_CONTEXT_MESSAGES);
                channelContext = (recent.messages ?? []).map((m: ChannelMsg) => ({
                  role: m.senderType === 'agent' ? 'assistant' : 'user',
                  content: m.senderType === 'agent' ? stripInternalBlocks(m.text) : `[${m.senderName}]: ${m.text}`,
                }));
              } catch { /* best-effort */ }
            }
            const roundId = `round_${Date.now()}`;
            const respondedAgents = new Set<string>([senderId]);
            void this.triggerAgentToAgentChain(
              filteredMentions, nameMap, senderName, senderId, cleanText, persistedMsgId,
              channelKey, orgId, agentManager, allAgentIds.length,
              { roundId, depth: 1, respondedAgents, allAgentIds },
            );
          } else {
            // No @mentions: deliver as mailbox notification so peers are aware
            for (const peerId of peerAgentIds) {
              try {
                agentManager.getAgent(peerId).enqueueToMailbox('a2a_message', {
                  summary: `Group chat message from ${senderName}`,
                  content: `[Group chat message from ${senderName}]:\n${cleanText}`,
                  extra: { senderId, senderName, channelKey, waitForReply: false },
                }, {
                  metadata: { senderId, senderName, senderRole: 'agent' },
                });
              } catch { /* agent may not exist */ }
            }
          }
        }

        return 'Message sent to group chat';
      },
      createGroupChat: async (
        name: string,
        creatorId: string,
        creatorName: string,
        memberIds: string[]
      ) => {
        if (this.storage?.groupChatRepo) {
          const members = memberIds.map(id => {
            try {
              const agentInfo = this.orgService.getAgentManager().getAgent(id);
              return { id, type: 'agent' as const, name: agentInfo?.config?.name ?? id };
            } catch { return { id, type: 'agent' as const, name: id }; }
          });
          if (creatorId && !members.some(m => m.id === creatorId)) {
            members.unshift({ id: creatorId, type: 'agent' as const, name: creatorName });
          }
          const gc = this.storage.groupChatRepo.create({
            orgId: 'default', name, creatorId, creatorName, members,
          });
          this.ws.broadcast({
            type: 'chat:group_created',
            payload: { chatId: gc.channelKey, name, creatorId, creatorName },
            timestamp: new Date().toISOString(),
          });
          return { id: gc.channelKey, name };
        }
        return { id: `group:custom:${Date.now().toString(36)}`, name };
      },
      listGroupChats: async () => {
        const teams = this.orgService.listTeamsWithMembers('default');
        const teamChats = teams.map(t => ({
          id: `group:${t.id}`,
          name: t.name,
          type: 'team',
          channelKey: `group:${t.id}`,
        }));
        const customChats = this.storage?.groupChatRepo
          ? this.storage.groupChatRepo.list('default').map((c: any) => ({
              id: c.channelKey,
              name: c.name,
              type: 'custom',
              channelKey: c.channelKey,
            }))
          : [];
        return [...teamChats, ...customChats];
      },
      getChannelMessages: async (channelKey: string, limit: number, before?: string) => {
        if (!this.storage) return { messages: [], hasMore: false };
        const result = this.storage.channelMessageRepo.getMessages(channelKey, limit, before);
        return {
          messages: result.messages.map((m: ChannelMsg) => ({
            id: m.id,
            senderName: m.senderName,
            senderType: m.senderType,
            text: stripInternalBlocks(m.text),
            replyToId: m.replyToId,
            replyToSender: m.replyToSender,
            replyToText: m.replyToText,
            createdAt: m.createdAt instanceof Date ? m.createdAt.toISOString() : String(m.createdAt),
          })),
          hasMore: result.hasMore,
        };
      },
    });

  }

  setSkillRegistry(registry: SkillRegistry): void {
    this.skillRegistry = registry;
    this.builderService = new BuilderService(
      this.orgService,
      registry,
      (msg) => this.ws?.broadcast(msg as Parameters<WSBroadcaster['broadcast']>[0]),
    );
    this.builderService.setTaskService(this.taskService);
  }

  getBuilderService(): BuilderService | undefined {
    return this.builderService;
  }

  /**
   * Returns a hub client that agents can use to search and install from Markus Hub.
   * Reads the hub token from ~/.markus/hub-token on each call.
   */
  getHubClient(): {
    search: (opts?: { type?: string; query?: string }) => Promise<Array<{ id: string; name: string; type: string; description: string; author: string; version?: string; downloads?: number }>>;
    downloadAndInstall: (itemId: string) => Promise<{ type: string; installed: unknown }>;
  } | undefined {
    if (!this.builderService) return undefined;
    const self = this;
    return {
      search: async (opts) => {
        const hubUrl = self.hubUrl;
        const token = self.readHubToken();
        const params = new URLSearchParams();
        if (opts?.type) params.set('type', opts.type);
        if (opts?.query) params.set('q', opts.query);
        const qs = params.toString();
        const headers: Record<string, string> = { 'Content-Type': 'application/json' };
        if (token) headers['Authorization'] = `Bearer ${token}`;
        const res = await fetch(`${hubUrl}/api/items${qs ? `?${qs}` : ''}`, { headers });
        if (!res.ok) throw new Error(`Hub search failed: ${res.status}`);
        const data = await res.json() as { items?: Array<Record<string, unknown>> };
        return (data.items ?? []).map((item: Record<string, unknown>) => ({
          id: item.id as string,
          name: item.name as string,
          type: (item.itemType ?? item.type ?? 'agent') as string,
          description: (item.description ?? '') as string,
          author: ((item.author as Record<string, unknown>)?.displayName ?? (item.author as Record<string, unknown>)?.username ?? '') as string,
          version: item.version as string | undefined,
          downloads: item.downloadCount as number | undefined,
        }));
      },
      downloadAndInstall: async (itemId) => {
        const hubUrl = self.hubUrl;
        const token = self.readHubToken();
        if (!token) throw new Error('Hub token not configured. Please login to Markus Hub first.');
        const headers: Record<string, string> = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` };
        const res = await fetch(`${hubUrl}/api/items/${itemId}/download`, { method: 'POST', headers });
        if (!res.ok) throw new Error(`Hub download failed: ${res.status}`);
        const data = await res.json() as { name: string; itemType: string; files?: Record<string, string>; config?: unknown; description?: string };
        const name = data.name;
        const slug = kebab(name, 'hub-pkg');
        const mode = (data.itemType === 'team' ? 'team' : data.itemType === 'skill' ? 'skill' : 'agent') as 'agent' | 'team' | 'skill';
        const typeDir = mode === 'agent' ? 'agents' : mode === 'team' ? 'teams' : 'skills';
        const artDir = join(homedir(), '.markus', 'builder-artifacts', typeDir, slug);
        mkdirSync(artDir, { recursive: true });
        if (data.files && Object.keys(data.files).length > 0) {
          for (const [fname, content] of Object.entries(data.files)) {
            const filePath = join(artDir, fname);
            mkdirSync(dirname(filePath), { recursive: true });
            writeFileSync(filePath, content, 'utf-8');
          }
        } else if (data.config) {
          writeFileSync(join(artDir, manifestFilename(mode as PackageType)), JSON.stringify(data.config, null, 2), 'utf-8');
        }
        return self.builderService!.installArtifact(mode, slug);
      },
    };
  }

  private readHubToken(): string | undefined {
    try {
      const tokenPath = join(homedir(), '.markus', 'hub-token');
      return existsSync(tokenPath) ? readFileSync(tokenPath, 'utf-8').trim() : undefined;
    } catch {
      return undefined;
    }
  }

  setBillingService(service: BillingService): void {
    this.billingService = service;
  }

  setAuditService(service: AuditService): void {
    this.auditService = service;
  }

  setHITLService(service: HITLService): void {
    this.hitlService = service;
    service.onNotification(n => {
      const event = {
        type: 'notification',
        payload: { notification: n },
        timestamp: new Date().toISOString(),
      };
      this.ws.sendToUser(n.targetUserId, event);
    });
  }

  setStorage(storage: StorageBridge): void {
    this.storage = storage;
  }

  setRemoteAgent(agent: { getStatus(): unknown; start(): Promise<void>; stop(): Promise<void>; onStatus(cb: (s: unknown) => void): () => void }): void {
    this.remoteAgent = agent;
    agent.onStatus((status) => {
      this.ws.broadcast({ type: 'remote:status', payload: status, timestamp: new Date().toISOString() });
    });
  }

  setRemoteAgentFactory(factory: () => Promise<{ getStatus(): unknown; start(): Promise<void>; stop(): Promise<void>; onStatus(cb: (s: unknown) => void): () => void } | null>): void {
    this.remoteAgentFactory = factory;
  }

  setGateway(gateway: ExternalAgentGateway, secret?: string): void {
    this.gateway = gateway;
    this.gatewaySecret = secret;
    this.initSyncHandler();
  }

  private initSyncHandler(): void {
    const self = this;
    this.syncHandler = new GatewaySyncHandler(
      {
        getTasksByAgent(agentId: string) {
          return self.taskService.getTasksByAgent(agentId).map(t => ({
            id: t.id, title: t.title, description: t.description,
            priority: t.priority, status: t.status,
            requirementId: t.requirementId,
            projectId: t.projectId,
          }));
        },
        updateTaskStatus(taskId: string, status: string, updatedBy?: string) {
          self.taskService.updateTaskStatus(taskId, status as TaskStatus, updatedBy);
        },
        createTask(req) {
          return self.taskService.createTask({
            title: req.title, description: req.description,
            priority: req.priority as TaskPriority,
            orgId: req.orgId,
            assignedAgentId: req.assignedAgentId,
            reviewerId: req.reviewerId,
            createdBy: req.createdBy,
          });
        },
      },
      {
        drainInbox(markusAgentId: string) {
          const queue = self.gatewayMessageQueue.get(markusAgentId) ?? [];
          self.gatewayMessageQueue.set(markusAgentId, []);
          return queue;
        },
        deliver(fromAgentId: string, toAgentId: string, content: string) {
          const fromAgent = self.orgService.getAgentManager().getAgent(fromAgentId);
          const queue = self.gatewayMessageQueue.get(toAgentId) ?? [];
          queue.push({
            id: `gwmsg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
            from: fromAgentId,
            fromName: fromAgent?.config?.name ?? fromAgentId,
            content,
            timestamp: new Date().toISOString(),
          });
          self.gatewayMessageQueue.set(toAgentId, queue);
        },
      },
      {
        updateStatus(_agentId: string, _status: 'idle' | 'working' | 'error') {},
        updateHeartbeat(_agentId: string) {},
      },
    );

    this.syncHandler.setTeamBridge({
      getColleagues(agentId: string, _orgId: string) {
        return self.orgService.getAgentManager().listAgents()
          .filter(a => a.id !== agentId)
          .map(a => ({ id: a.id, name: a.name, role: a.role, status: a.status }));
      },
      getManager(agentId: string, _orgId: string) {
        const agents = self.orgService.getAgentManager().listAgents();
        const mgr = agents.find(a => a.agentRole === 'manager' && a.id !== agentId);
        return mgr ? { id: mgr.id, name: mgr.name } : undefined;
      },
    });

    this.syncHandler.setProjectBridge({
      getProjects(orgId: string) {
        if (!self.projectService) return [];
        return self.projectService.listProjects(orgId).map(p => ({
          id: p.id,
          name: p.name,
        }));
      },
      getActiveRequirements(orgId: string) {
        if (!self.requirementService) return [];
        return self.requirementService.listRequirements({ orgId })
          .filter(r => r.status === 'in_progress')
          .map(r => ({
            id: r.id,
            title: r.title,
            status: r.status,
            priority: r.priority,
            projectId: r.projectId,
          }));
      },
    });
  }

  setReviewService(svc: ReviewService): void {
    this.reviewService = svc;
  }

  setTemplateRegistry(registry: TemplateRegistry): void {
    this.templateRegistry = registry;
  }

  setLLMRouter(router: LLMRouter): void {
    this.llmRouter = router;
  }

  setConfigPath(configPath: string): void {
    this.markusConfigPath = configPath;
  }

  setHubUrl(url: string): void {
    this.hubUrl = url;
  }

  setWebUiDir(dir: string): void {
    this.webUiDir = dir;
  }

  setFileStorage(provider: LocalFileStorageProvider): void {
    this.fileStorage = provider;
  }

  initWorkflowEngine(): WorkflowEngine {
    const agentManager = this.orgService.getAgentManager();
    const executor: WorkflowExecutor = {
      executeStep: async (
        agentId: string,
        taskDescription: string,
        input: Record<string, unknown>
      ) => {
        const agent = agentManager.getAgent(agentId);
        const reply = await agent.sendMessage(
          taskDescription,
          'workflow-engine',
          { name: 'workflow', role: 'system' },
          {
            sourceType: 'system_event',
            sessionId: `sys_${agentId}_${Date.now()}`,
          }
        );
        return { reply, input };
      },
      findAgent: (skills: string[]) => {
        const agents = agentManager.listAgents();
        const found = agents.find(a =>
          skills.some(
            s =>
              a.role?.toLowerCase().includes(s.toLowerCase()) ||
              a.agentRole?.toLowerCase().includes(s.toLowerCase())
          )
        );
        return found?.id;
      },
    };
    this.workflowEngine = new WorkflowEngine(executor);
    return this.workflowEngine;
  }

  getTeamTemplateRegistry(): TeamTemplateRegistry {
    return this.teamTemplateRegistry;
  }

  /** Ensure at least one admin/owner user exists; called once after storage init.
   *  Migrates legacy 'default' user rows to a real auto-generated ID.
   *  Returns the owner's user ID (existing/migrated or newly created). */
  async ensureAdminUser(orgId: string): Promise<string> {
    if (!this.storage) return genUserId();
    const allUsers = await this.storage.userRepo.listByOrg(orgId);

    // Check for existing owner with a password
    const existingOwner = allUsers.find((u: any) => u.role === 'owner' && u.passwordHash);
    if (existingOwner) {
      // Migrate legacy 'default' ID to a proper auto-generated ID
      if (existingOwner.id === 'default') {
        const newId = genUserId();
        this.storage.userRepo.migrateDefaultId(newId);
        log.info("Migrated legacy owner id='default'", { newId });
        return newId;
      }
      return existingOwner.id as string;
    }

    // No owner found — create fresh
    const adminPassword = process.env['ADMIN_PASSWORD'] ?? 'markus123';
    const hash = await hashPassword(adminPassword);
    const ownerId = genUserId();
    await this.storage.userRepo.upsert({
      id: ownerId,
      orgId,
      name: 'Admin',
      email: 'admin@markus.local',
      role: 'owner',
      passwordHash: hash,
    });
    log.info('Created admin user (admin@markus.local)', { userId: ownerId });
    return ownerId;
  }

  private get jwtSecret(): string {
    return process.env['JWT_SECRET'] ?? 'markus-dev-secret-change-in-prod';
  }

  private get authEnabled(): boolean {
    return process.env['AUTH_ENABLED'] !== 'false';
  }

  /** Returns user payload from JWT cookie, or null if not authenticated */
  private async getAuthUser(
    req: IncomingMessage
  ): Promise<{ userId: string; orgId: string; role: string } | null> {
    if (!this.authEnabled) return { userId: 'anonymous', orgId: 'default', role: 'owner' };
    const cookies = parseCookies(req.headers['cookie']);
    const token = cookies['markus_token'];
    if (!token) return null;
    const payload = await verifyToken(token, this.jwtSecret);
    if (!payload) return null;
    return payload as { userId: string; orgId: string; role: string };
  }

  /** Returns user or sends 401 and returns null */
  private async requireAuth(
    req: IncomingMessage,
    res: ServerResponse
  ): Promise<{ userId: string; orgId: string; role: string } | null> {
    const user = await this.getAuthUser(req);
    if (!user) {
      this.json(res, 401, { error: 'Unauthorized' });
      return null;
    }
    return user;
  }

  /** Persist a chat turn (user + assistant) to DB if storage is available */
  private async persistChatTurn(
    agentId: string,
    userMessage: string,
    reply: string,
    senderId?: string,
    tokensUsed = 0,
    metadata?: unknown
  ): Promise<void> {
    if (!this.storage) return;
    try {
      const sessions = await this.storage.chatSessionRepo.getSessionsByAgent(agentId, 1, senderId);
      let session = sessions[0];
      if (!session) {
        session = await this.storage.chatSessionRepo.createSession(agentId, senderId);
      }
      const title = !session.title ? userMessage.slice(0, 60) : undefined;
      await this.storage.chatSessionRepo.appendMessage(session.id, agentId, 'user', userMessage, 0);
      await this.storage.chatSessionRepo.appendMessage(
        session.id,
        agentId,
        'assistant',
        reply,
        tokensUsed,
        metadata
      );
      await this.storage.chatSessionRepo.updateLastMessage(session.id, title);
    } catch (err) {
      log.warn('Failed to persist chat turn', { error: String(err) });
    }
  }

  // ── Agent-to-agent reply storm prevention ───────────────────────────────────
  private static readonly A2A_MAX_DEPTH = 3;
  private static readonly A2A_COOLDOWN_MS = 30_000;
  /** Per-channel cooldown tracker: channel → agentId → last reply timestamp */
  private a2aCooldowns = new Map<string, Map<string, number>>();

  private cleanStaleCooldowns(): void {
    const now = Date.now();
    for (const [channel, agentMap] of this.a2aCooldowns) {
      for (const [agentId, lastReply] of agentMap) {
        if (now - lastReply > APIServer.A2A_COOLDOWN_MS * 2) {
          agentMap.delete(agentId);
        }
      }
      if (agentMap.size === 0) {
        this.a2aCooldowns.delete(channel);
      }
    }
  }

  /** Extract @mentions from agent reply text. Returns agent names found.
   *  Handles multi-word names (e.g. "Ryan 莱恩") by checking known names after each @ sign.
   *  Also matches partial names: "@Sofia" matches "Sofia 索菲亚" if "Sofia" is a token in the name. */
  private parseAgentMentions(text: string, knownAgentNames: string[]): string[] {
    const mentioned = new Set<string>();

    // Build lookup: full names sorted longest-first, plus individual tokens → full name
    const sorted = [...knownAgentNames].sort((a, b) => b.length - a.length);
    const tokenToName = new Map<string, string>();
    for (const name of sorted) {
      // Each whitespace-separated token maps back to the full name
      for (const token of name.split(/\s+/)) {
        if (token.length >= 2) {
          const key = token.toLowerCase();
          if (!tokenToName.has(key)) tokenToName.set(key, name);
        }
      }
    }

    let idx = 0;
    while (idx < text.length) {
      const atPos = text.indexOf('@', idx);
      if (atPos < 0) break;

      // Check @[Name With Spaces] bracket syntax first
      if (text[atPos + 1] === '[') {
        const close = text.indexOf(']', atPos + 2);
        if (close > atPos + 2) {
          const bracketed = text.slice(atPos + 2, close);
          const match = sorted.find(n => n.toLowerCase() === bracketed.toLowerCase());
          if (match) mentioned.add(match);
          idx = close + 1;
          continue;
        }
      }

      const after = text.slice(atPos + 1);
      const afterLower = after.toLowerCase();

      // Try 1: full name prefix match (e.g. "@Ryan 莱恩" matches "Ryan 莱恩")
      const fullMatch = sorted.find(n => afterLower.startsWith(n.toLowerCase()));
      if (fullMatch) {
        mentioned.add(fullMatch);
        idx = atPos + 1 + fullMatch.length;
        continue;
      }

      // Try 2: single-token match (e.g. "@Sofia" matches "Sofia 索菲亚" via token "Sofia")
      // Extract the word right after @ (up to next space, punctuation, or CJK boundary)
      const tokenMatch = after.match(/^(\S+)/);
      if (tokenMatch) {
        const token = tokenMatch[1]!.toLowerCase()
          .replace(/[,，。！？!?;；:：、()（）[\]【】]+$/, ''); // strip trailing punctuation
        const resolved = tokenToName.get(token);
        if (resolved) {
          mentioned.add(resolved);
          idx = atPos + 1 + tokenMatch[1]!.length;
          continue;
        }
      }

      idx = atPos + 1;
    }
    return [...mentioned];
  }

  /** Resolve display name for a group channel */
  private resolveChannelName(channelKey: string): string {
    if (channelKey.startsWith('group:custom:') && this.storage?.groupChatRepo) {
      const gc = this.storage.groupChatRepo.getByChannelKey(channelKey);
      if (gc) return gc.name;
    }
    if (channelKey.startsWith('group:')) {
      const teamId = channelKey.replace(/^group:/, '');
      const team = this.orgService.getTeam(teamId);
      if (team) return team.name;
    }
    return channelKey;
  }

  /** Resolve human member IDs for a group channel (custom or team-based) */
  private resolveChannelHumanIds(channelKey: string): string[] {
    if (channelKey.startsWith('group:custom:') && this.storage?.groupChatRepo) {
      return this.storage.groupChatRepo.getHumanMemberIds(channelKey);
    }
    if (channelKey.startsWith('group:')) {
      const teamId = channelKey.replace(/^group:/, '');
      const team = this.orgService.getTeam(teamId);
      return team?.humanMemberIds ?? [];
    }
    return [];
  }

  /** Build name→id lookup for agents in a channel */
  private buildAgentNameMap(agentIds: string[], agentManager: AgentManager): Map<string, string> {
    const map = new Map<string, string>();
    for (const id of agentIds) {
      try { map.set(agentManager.getAgent(id).config.name, id); } catch { /* skip */ }
    }
    return map;
  }

  /**
   * Process a single agent's reply in a group chat broadcast.
   * Each agent decides independently whether to respond.
   * Replies are persisted and broadcast via WebSocket.
   * If the agent @mentions other agents, a chain reply is triggered (with storm prevention).
   */
  private async processGroupChatReply(
    agentId: string,
    userMessage: string,
    senderId: string,
    senderInfo: { name: string; role: string } | undefined,
    channel: string,
    orgId: string,
    channelContext: Array<{ role: string; content: string }>,
    agentManager: AgentManager,
    teamSize: number,
    opts?: { mentionedNames?: string[]; replyToAgentName?: string; replyToText?: string; replyToMsgId?: string },
    chainCtx?: { roundId: string; depth: number; respondedAgents: Set<string>; allAgentIds: string[] },
  ): Promise<void> {
    try {
      const agent = agentManager.getAgent(agentId);
      const agentName = agent.config.name;

      const isA2A = !!chainCtx && chainCtx.depth > 0;
      const hasReplyTarget = !!opts?.replyToAgentName;
      const hasMentions = !!opts?.mentionedNames?.length;
      const isTargeted = hasReplyTarget || hasMentions;

      const targetNames = new Set<string>();
      if (opts?.replyToAgentName) targetNames.add(opts.replyToAgentName);
      if (opts?.mentionedNames) opts.mentionedNames.forEach(n => targetNames.add(n));
      const thisAgentIsTarget = targetNames.has(agentName);

      const prefixLines = [
        `[GROUP CHAT — ${teamSize} team members | You are: ${agentName}]`,
        `[CHANNEL] channel_key="${channel}" — You already have the most recent ~${CHANNEL_CONTEXT_MESSAGES} messages in your context.`,
        '',
      ];

      if (isA2A) {
        prefixLines.push(`[AGENT COLLABORATION] This message is from a fellow agent (${opts?.replyToAgentName ?? 'teammate'}), not from the user.`);
        prefixLines.push('You were @mentioned because your expertise is needed. Respond concisely to the specific request.');
        prefixLines.push('You may @mention another agent if (and ONLY if) you genuinely need their specific expertise to answer.');
        prefixLines.push('Do NOT @mention agents just to be polite or to pass the conversation along.');
        prefixLines.push('');
      }

      if (isTargeted && !isA2A) {
        if (hasReplyTarget) {
          prefixLines.push(`[REPLY] The user is replying to ${opts!.replyToAgentName}'s message: "${(opts!.replyToText ?? '').slice(0, 200)}"`);
        }
        if (hasMentions) {
          prefixLines.push(`[MENTIONED] The user @mentioned: ${opts!.mentionedNames!.join(', ')}`);
        }
        prefixLines.push('');
        if (thisAgentIsTarget) {
          prefixLines.push('>>> You are the target of this message. You SHOULD respond. <<<');
        } else {
          prefixLines.push('>>> STOP. This message is NOT for you. The user is talking to ' + [...targetNames].join(', ') + ', not you.');
          prefixLines.push('You MUST respond with exactly: [NO_RESPONSE]');
          prefixLines.push('The ONLY exception: you are directly contradicted by a factual error. Offering opinions, agreement, "me too", or generic help does NOT count. <<<');
        }
      } else if (!isA2A) {
        prefixLines.push('This is an open group message (no specific @mention or reply target).');
      }

      // Static group chat rules (silence-by-default, @mention routing, processing
      // checklist) are now in the system prompt via scenario='group_chat'.
      // Only per-message variable parts remain here.
      if (chainCtx?.allAgentIds) {
        prefixLines.push('');
        const rosterLines = ['TEAM MEMBERS (use exact format for @mentions):'];
        for (const aid of chainCtx.allAgentIds) {
          try {
            const a = agentManager.getAgent(aid);
            const name = a.config.name;
            const fmt = name.includes(' ') ? `@[${name}]` : `@${name}`;
            rosterLines.push(`  ${fmt}${aid === agentId ? ' (you)' : ''}`);
          } catch { /* skip */ }
        }
        prefixLines.push(rosterLines.join('\n'));
      }
      if (isTargeted && !thisAgentIsTarget && !isA2A) {
        prefixLines.push('');
        prefixLines.push('REMINDER: This message is directed at ' + [...targetNames].join(', ') + '. You are ' + agentName + '. Respond ONLY with [NO_RESPONSE].');
      }

      prefixLines.push('---', '');
      const groupChatPrefix = prefixLines.join('\n');

      const toolEvents: Array<{ tool: string; status: 'done' | 'error'; arguments?: unknown; result?: string; durationMs?: number }> = [];
      const reply = await agent.sendMessage(
        groupChatPrefix + userMessage,
        senderId,
        senderInfo,
        {
          sourceType: isA2A ? 'a2a_message' : 'human_chat',
          scenario: isA2A ? 'a2a' : 'group_chat',
          channelContext,
          channelKey: channel,
          directMention: thisAgentIsTarget,
          toolEventCollector: toolEvents,
        }
      );

      const emitNoResponse = () => {
        const evt = {
          type: 'chat:agent_no_response' as const,
          payload: { channel, agentId },
          timestamp: new Date().toISOString(),
        };
        if (channel.startsWith('dm:') || channel.startsWith('notes:')) {
          const parts = channel.startsWith('notes:')
            ? [channel.slice(6)]
            : channel.slice(3).split(':');
          this.ws.sendToUsers(parts, evt);
        } else {
          const humanIds = this.resolveChannelHumanIds(channel);
          if (humanIds.length > 0) this.ws.sendToUsers(humanIds, evt);
          else this.ws.broadcast(evt);
        }
      };

      if (!reply || !reply.trim() || /\[NO_RESPONSE\b/i.test(reply)) {
        emitNoResponse();
        return;
      }

      const { thinking, clean: rawClean } = extractThinkBlocks(reply);
      // Strip [NO_RESPONSE] and its variants: [NO_RESPONSE — ...], [NO_RESPONSE: ...], etc.
      const noResponseStripped = rawClean.replace(/\[NO_RESPONSE[^\]]*\]/gi, '').trim();
      // If the entire message was a NO_RESPONSE block (even without closing bracket), suppress
      if (/^\[NO_RESPONSE\b/i.test(rawClean.trim())) {
        emitNoResponse();
        return;
      }
      // Catch creative "no response" phrasings the LLM invents instead of [NO_RESPONSE]:
      // e.g. "[context check — no response needed]", "[no response necessary]", "[silent]"
      const NO_RESPONSE_CREATIVE_RE = /^\[(?:context check|no response|silent|listening|observing|monitoring|watching|noting|acknowledged?)[^\]]*\]$/i;
      if (NO_RESPONSE_CREATIVE_RE.test(rawClean.trim())) {
        log.info('Suppressed creative no-response variant', { agentId, original: rawClean.trim().slice(0, 100) });
        emitNoResponse();
        return;
      }
      // Strip lines that look like raw tool invocations leaked into the reply.
      // Two patterns:
      //   1. Known Markus tool names — slash is optional (agent may write "recall_context" or "/recall_context")
      //   2. Generic slash commands from other platforms — slash is REQUIRED to avoid false positives
      //      (e.g. "/history 30" is a command, but "List the points" is normal text)
      const KNOWN_TOOL_RE = /^\s*\/?(?:recall_context|memory_search|memory_save|task_get|task_list|task_comment|requirement_get|requirement_comment|file_read|agent_send_message|agent_send_group_message|check_mailbox|update_working_memory|clear_working_memory|defer_mailbox_item|drop_mailbox_item|prioritize_mailbox_item|notify_user|recall_activity)\b/i;
      const SLASH_CMD_RE = /^\s*\/(?:history|help|status|list|search|get|set|info|ping|who|whois|me|join|leave|invite|kick|ban|mute|unmute|clear|purge|poll|remind|note|todo|roll|flip|ask)\b/i;
      const cleanReply = noResponseStripped.split('\n')
        .filter(line => !KNOWN_TOOL_RE.test(line) && !SLASH_CMD_RE.test(line))
        .join('\n').trim();
      if (!cleanReply) {
        log.warn('Suppressed raw tool command leak', { agentId, original: noResponseStripped.slice(0, 200) });
        emitNoResponse();
        return;
      }

      const metadata: Record<string, unknown> = {};
      if (thinking.length > 0) metadata['thinking'] = thinking;
      if (toolEvents.length > 0) metadata['toolCalls'] = toolEvents;
      if (isA2A && opts?.replyToAgentName) {
        metadata['replyToAgent'] = opts.replyToAgentName;
      }

      // Persist agent reply
      let persistedMsgId: string | undefined;
      if (this.storage) {
        const saved = await this.storage.channelMessageRepo.append({
          orgId,
          channel,
          senderId: agentId,
          senderType: 'agent',
          senderName: agentName,
          text: cleanReply,
          mentions: [],
          metadata: Object.keys(metadata).length > 0 ? metadata as any : undefined,
          replyToId: isA2A ? opts?.replyToMsgId : undefined,
        });
        persistedMsgId = saved.id;
      }

      // Send via WebSocket (scoped for DM/notes channels)
      const replyEvent = {
        type: 'chat:message' as const,
        payload: {
          channel,
          senderId: agentId,
          senderType: 'agent',
          senderName: agentName,
          text: cleanReply,
          metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
          ...(isA2A ? {
            replyToId: opts?.replyToMsgId,
            replyToSender: opts?.replyToAgentName,
            replyToText: (opts?.replyToText ?? '').slice(0, 120),
          } : {}),
        },
        timestamp: new Date().toISOString(),
      };
      if (channel.startsWith('dm:') || channel.startsWith('notes:')) {
        const participants = channel.startsWith('notes:')
          ? [channel.slice(6)]
          : channel.slice(3).split(':');
        this.ws.sendToUsers(participants, replyEvent);
      } else {
        const humanIds = this.resolveChannelHumanIds(channel);
        if (humanIds.length > 0) this.ws.sendToUsers(humanIds, replyEvent);
        else this.ws.broadcast(replyEvent);
      }

      // ── Agent-to-agent chain: parse @mentions in the reply ──
      const allAgentIds = chainCtx?.allAgentIds ?? [];
      if (allAgentIds.length > 0) {
        const nameMap = this.buildAgentNameMap(allAgentIds, agentManager);
        const mentionedNames = this.parseAgentMentions(cleanReply, [...nameMap.keys()]);
        const filteredMentions = mentionedNames.filter(n => n !== agentName);
        log.info('A2A mention scan', { agentName, mentionedNames, filteredMentions, depth: chainCtx?.depth ?? 0 });

        if (filteredMentions.length > 0) {
          const depth = (chainCtx?.depth ?? 0) + 1;
          const roundId = chainCtx?.roundId ?? `round_${Date.now()}`;
          const responded = chainCtx?.respondedAgents ?? new Set<string>();
          responded.add(agentId);

          void this.triggerAgentToAgentChain(
            filteredMentions, nameMap, agentName, agentId, cleanReply, persistedMsgId,
            channel, orgId, agentManager, teamSize,
            { roundId, depth, respondedAgents: responded, allAgentIds },
          );
        }
      }
    } catch (err) {
      log.warn('Group chat agent reply failed', { agentId, error: String(err) });

      if (this.storage) {
        try {
          const errDetail = String(err).slice(0, 500);
          await this.storage.channelMessageRepo.append({
            orgId,
            channel,
            senderId: agentId,
            senderType: 'system',
            senderName: 'System',
            text: `⚠ AI service error: ${errDetail}`,
            mentions: [],
          });
        } catch { /* best-effort */ }
      }
    }
  }

  /**
   * Trigger agent-to-agent chain replies with 3-layer storm prevention:
   * 1. Depth limit (max 3 hops)
   * 2. Per-round dedup (each agent responds at most once per conversation round)
   * 3. Cooldown window (same agent won't be triggered twice within 30s on same channel)
   */
  private async triggerAgentToAgentChain(
    mentionedNames: string[],
    nameMap: Map<string, string>,
    fromAgentName: string,
    fromAgentId: string,
    fromText: string,
    fromMsgId: string | undefined,
    channel: string,
    orgId: string,
    agentManager: AgentManager,
    teamSize: number,
    chainCtx: { roundId: string; depth: number; respondedAgents: Set<string>; allAgentIds: string[] },
  ): Promise<void> {
    this.cleanStaleCooldowns();

    // Layer 1: depth limit — deliver to mailbox without chain capability
    if (chainCtx.depth > APIServer.A2A_MAX_DEPTH) {
      log.info('A2A chain depth limit — delivering to mailbox without chain', { channel, depth: chainCtx.depth, roundId: chainCtx.roundId });
      for (const targetName of mentionedNames) {
        const targetId = nameMap.get(targetName);
        if (!targetId) continue;
        try {
          agentManager.getAgent(targetId).enqueueToMailbox('a2a_message', {
            summary: `@mention from ${fromAgentName} (chain depth limit)`,
            content: `[From @${fromAgentName}]: ${fromText}`,
            extra: { senderId: fromAgentId, senderName: fromAgentName, channelKey: channel, directMention: true, throttled: true },
          }, { metadata: { senderId: fromAgentId, senderName: fromAgentName, senderRole: 'agent' } });
        } catch { /* agent may not exist */ }
      }
      return;
    }

    const now = Date.now();
    if (!this.a2aCooldowns.has(channel)) {
      this.a2aCooldowns.set(channel, new Map());
    }
    const channelCooldowns = this.a2aCooldowns.get(channel)!;

    // Build fresh channel context — provide ample history for informed replies
    let channelContext: Array<{ role: string; content: string }> = [];
    if (this.storage) {
      try {
        const recent = await this.storage.channelMessageRepo.getMessages(channel, CHANNEL_CONTEXT_MESSAGES);
        channelContext = (recent.messages ?? []).map((m: ChannelMsg) => ({
          role: m.senderType === 'agent' ? 'assistant' : 'user',
          content: m.senderType === 'agent'
            ? stripInternalBlocks(m.text)
            : `[${m.senderName}]: ${m.text}`,
        }));
      } catch { /* best-effort */ }
    }

    for (const targetName of mentionedNames) {
      const targetId = nameMap.get(targetName);
      if (!targetId) continue;

      // Layer 2: per-round dedup — deliver to mailbox without chain capability
      if (chainCtx.respondedAgents.has(targetId)) {
        log.info('A2A throttled (already responded) — delivering to mailbox', { targetName, roundId: chainCtx.roundId });
        try {
          agentManager.getAgent(targetId).enqueueToMailbox('a2a_message', {
            summary: `@mention from ${fromAgentName} (round dedup)`,
            content: `[From @${fromAgentName}]: ${fromText}`,
            extra: { senderId: fromAgentId, senderName: fromAgentName, channelKey: channel, directMention: true, throttled: true },
          }, { metadata: { senderId: fromAgentId, senderName: fromAgentName, senderRole: 'agent' } });
        } catch { /* agent may not exist */ }
        continue;
      }

      // Layer 3: cooldown window — deliver to mailbox without chain capability
      const lastReply = channelCooldowns.get(targetId) ?? 0;
      if (now - lastReply < APIServer.A2A_COOLDOWN_MS) {
        log.info('A2A throttled (cooldown) — delivering to mailbox', { targetName, channel, cooldownRemaining: APIServer.A2A_COOLDOWN_MS - (now - lastReply) });
        try {
          agentManager.getAgent(targetId).enqueueToMailbox('a2a_message', {
            summary: `@mention from ${fromAgentName} (cooldown)`,
            content: `[From @${fromAgentName}]: ${fromText}`,
            extra: { senderId: fromAgentId, senderName: fromAgentName, channelKey: channel, directMention: true, throttled: true },
          }, { metadata: { senderId: fromAgentId, senderName: fromAgentName, senderRole: 'agent' } });
        } catch { /* agent may not exist */ }
        continue;
      }
      channelCooldowns.set(targetId, now);

      const a2aMessage = `[From @${fromAgentName}]: ${fromText}`;

      void this.processGroupChatReply(
        targetId,
        a2aMessage,
        fromAgentId,
        { name: fromAgentName, role: 'agent' },
        channel,
        orgId,
        channelContext,
        agentManager,
        teamSize,
        {
          mentionedNames: [targetName],
          replyToAgentName: fromAgentName,
          replyToText: fromText.slice(0, 200),
          replyToMsgId: fromMsgId,
        },
        chainCtx,
      );
    }
  }

  /** Persist the user message first (before LLM), returns session id for subsequent assistant persistence.
   *  When sessionId is provided, appends to that session; when null/undefined, creates a new session. */
  private async persistUserMessage(
    agentId: string,
    userMessage: string,
    senderId?: string,
    images?: string[],
    sessionId?: string | null,
  ): Promise<string | null> {
    if (!this.storage) return null;
    try {
      let session: { id: string; title: string | null } | undefined;
      if (sessionId) {
        session = await this.storage.chatSessionRepo.getSession(sessionId) ?? undefined;
      }
      if (!session) {
        session = await this.storage.chatSessionRepo.createSession(agentId, senderId);
      }
      const title = !session!.title ? userMessage.slice(0, 60) : undefined;
      const meta = images?.length ? { images } : undefined;
      await this.storage.chatSessionRepo.appendMessage(session!.id, agentId, 'user', userMessage, 0, meta);
      if (title) await this.storage.chatSessionRepo.updateLastMessage(session!.id, title);
      return session!.id;
    } catch (err) {
      log.warn('Failed to persist user message', { error: String(err) });
      return null;
    }
  }

  /** Persist the assistant reply after LLM completes */
  private async persistAssistantMessage(
    sessionId: string | null,
    agentId: string,
    reply: string,
    tokensUsed = 0,
    metadata?: unknown
  ): Promise<void> {
    if (!this.storage || !sessionId) return;
    try {
      const msg = await this.storage.chatSessionRepo.appendMessage(
        sessionId,
        agentId,
        'assistant',
        reply,
        tokensUsed,
        metadata
      );
      await this.storage.chatSessionRepo.updateLastMessage(sessionId);
      if (msg?.id) {
        this.ws.broadcastUnreadUpdate(`session:${sessionId}`, msg.id);
      }
    } catch (err) {
      log.warn('Failed to persist assistant message', { error: String(err) });
    }
  }

  private triggerSecretaryWelcome(userId: string, userName: string, userRole: string): void {
    try {
      const mgr = this.orgService.getAgentManager();
      const agentList = mgr.listAgents();
      const secretaryInfo = agentList.find(a =>
        a.agentRole === 'secretary' || a.role?.toLowerCase() === 'secretary'
      );
      if (!secretaryInfo) return;
      const secretary = mgr.getAgent(secretaryInfo.id);
      const welcomeMsg = `[SYSTEM] A new team member just joined: "${userName}" (role: ${userRole}, id: ${userId}). They have completed their account setup. As their Secretary, proactively guide them through the system capabilities. Send them a welcome message using notify_user (target the new user by their id: ${userId}) explaining what they can do in Markus — projects, tasks, deliverables, team collaboration, and how to work with AI agents. Help them get started with their first steps.`;
      secretary.sendMessage(welcomeMsg, userId, {
        name: userName,
        role: userRole,
        isFirstConversation: true,
      }, { sourceType: 'human_chat', scenario: 'chat' });
    } catch (err) {
      log.warn('Failed to trigger secretary welcome for new user', { userId, error: String(err) });
    }
  }

  start(): void {
    this.server = createServer((req, res) => this.handleRequest(req, res));
    this.ws.attach(this.server);
    this.server.listen(this.port, '0.0.0.0', () => {
      log.info(`API server listening on 0.0.0.0:${this.port} (HTTP + WebSocket)`);
    });
  }

  stop(): void {
    this.server?.close();
  }

  getWSBroadcaster(): WSBroadcaster {
    return this.ws;
  }

  private handleRequest(req: IncomingMessage, res: ServerResponse): void {
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');

    if (req.method === 'OPTIONS') {
      res.writeHead(204);
      res.end();
      return;
    }

    // BUG-005: Validate Content-Type for POST/PUT/PATCH before routing (before auth)
    if (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') {
      const contentType = String(req.headers['content-type'] ?? '').toLowerCase();
      const isJson = contentType.includes('application/json');
      const isMultipart = contentType.includes('multipart/form-data');
      if (!isJson && !isMultipart) {
        this.json(res, 415, { error: 'Content-Type must be application/json or multipart/form-data' });
        return;
      }
    }

    const url = new URL(req.url ?? '/', `http://localhost:${this.port}`);
    const path = url.pathname;

    this.route(req, res, path, url).catch(error => {
      const msg = error instanceof Error ? error.message : String(error);
      log.error('Request handler error', { error: msg, path });
      if (res.headersSent) {
        // SSE or chunked stream already started — send an error event and close gracefully
        try {
          res.write(`data: ${JSON.stringify({ type: 'error', message: msg })}\n\n`);
        } catch {
          /* ignore if write also fails */
        }
        res.end();
      } else {
        if (msg.startsWith('CONTENT_TYPE_ERROR:')) {
          this.json(res, 415, { error: 'Content-Type must be application/json' });
        } else if (msg.startsWith('BODY_PARSE_ERROR:')) {
          this.json(res, 400, { error: 'Invalid request body' });
        } else {
          this.json(res, 500, { error: 'Internal server error' });
        }
      }
    });
  }

  private async route(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
    url: URL
  ): Promise<void> {
    // BUG-003: Pre-read and validate body for POST/PUT/PATCH at route level.
    // This ensures body validation happens before any route-specific logic,
    // so invalid bodies (null/array) are caught even for routes without readBody.
    // Skip for multipart/form-data (image uploads handle body parsing themselves).
    if (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') {
      const ct = String(req.headers['content-type'] ?? '').toLowerCase();
      if (!ct.includes('multipart/form-data')) {
        await this.readBody(req);
      }
    }

    // ── Auth endpoints (no auth required) ──────────────────────────────────

    // System initialization status — tells frontend whether to show Login or InitialSetup
    if (path === '/api/auth/status' && req.method === 'GET') {
      if (!this.storage || !this.authEnabled) {
        this.json(res, 200, { initialized: true });
        return;
      }
      const allUsers = await this.storage.userRepo.listByOrg('default');
      const hasRealUsers = allUsers.some((u: any) => u.passwordHash && u.email !== 'admin@markus.local');
      this.json(res, 200, { initialized: hasRealUsers });
      return;
    }

    // First-time system initialization — creates admin user (only works when no real users exist)
    if (path === '/api/auth/init' && req.method === 'POST') {
      if (!this.storage) {
        this.json(res, 503, { error: 'Storage not available' });
        return;
      }
      // Only allow when system is not yet initialized
      const allUsers = await this.storage.userRepo.listByOrg('default');
      const hasRealUsers = allUsers.some((u: any) => u.passwordHash && u.email !== 'admin@markus.local');
      if (hasRealUsers) {
        this.json(res, 403, { error: 'System already initialized' });
        return;
      }
      const body = await this.readBody(req);
      const name = ((body['name'] as string) ?? '').trim();
      const email = ((body['email'] as string) ?? '').trim().toLowerCase();
      const password = (body['password'] as string) ?? '';
      if (!name || !email || !password) {
        this.json(res, 400, { error: 'name, email and password are required' });
        return;
      }
      if (password.length < 6) {
        this.json(res, 400, { error: 'Password must be at least 6 characters' });
        return;
      }
      const hash = await hashPassword(password);
      // Check if there's an unclaimed placeholder admin to adopt
      const placeholder = allUsers.find((u: any) => u.role === 'owner' && u.email === 'admin@markus.local');
      let userId: string;
      if (placeholder) {
        userId = placeholder.id as string;
        this.storage.userRepo.updateProfile(userId, { name, email });
        await this.storage.userRepo.updatePassword(userId, hash);
      } else {
        userId = genUserId();
        await this.storage.userRepo.upsert({
          id: userId, orgId: 'default', name, email, role: 'owner', passwordHash: hash,
        });
      }
      // Sync in-memory identity so agents see the real name immediately
      // (DB was already updated above — this only touches the in-memory map)
      this.orgService.syncHumanIdentity(userId, 'default', name, 'owner', email);
      await this.storage.userRepo.updateLastLogin(userId);
      const exp = Math.floor(Date.now() / 1000) + 7 * 24 * 3600;
      const token = await signToken(
        { userId, orgId: 'default', role: 'owner', exp },
        this.jwtSecret
      );
      res.setHeader(
        'Set-Cookie',
        `markus_token=${token}; HttpOnly; SameSite=Strict; Path=/; Max-Age=${7 * 24 * 3600}`
      );
      this.json(res, 200, {
        user: { id: userId, name, email, role: 'owner', orgId: 'default' },
        needsOnboarding: true,
      });
      return;
    }

    if (path === '/api/auth/login' && req.method === 'POST') {
      const body = await this.readBody(req);
      const email = ((body['email'] as string) ?? '').trim().toLowerCase();
      const password = (body['password'] as string) ?? '';

      if (!this.authEnabled) {
        this.json(res, 200, { user: { id: 'anonymous', name: 'Admin', role: 'owner' } });
        return;
      }

      let userRow = this.storage ? await this.storage.userRepo.findByEmail(email) : null;

      // First-time login: if the email isn't found, check if there's an unclaimed
      // admin user (still using the placeholder email). If the password matches,
      // adopt that admin user with the provided email.
      if (!userRow && this.storage) {
        const allUsers = await this.storage.userRepo.listByOrg('default');
        const unclaimedOwner = allUsers.find((u: any) =>
          u.role === 'owner' && !u.lastLoginAt && u.email === 'admin@markus.local'
        );
        if (unclaimedOwner && unclaimedOwner.passwordHash) {
          const ownerPasswordValid = await verifyPassword(password, unclaimedOwner.passwordHash);
          if (ownerPasswordValid) {
            this.storage.userRepo.updateProfile(unclaimedOwner.id, { email });
            userRow = { ...unclaimedOwner, email } as typeof userRow;
          }
        }
      }

      if (!userRow || !userRow.passwordHash) {
        this.json(res, 401, { error: 'Invalid email or password' });
        return;
      }
      const valid = await verifyPassword(password, userRow.passwordHash);
      if (!valid) {
        this.json(res, 401, { error: 'Invalid email or password' });
        return;
      }
      const isFirstLogin = !userRow.lastLoginAt;
      await this.storage!.userRepo.updateLastLogin(userRow.id);
      const exp = Math.floor(Date.now() / 1000) + 7 * 24 * 3600;
      const token = await signToken(
        { userId: userRow.id, orgId: userRow.orgId, role: userRow.role, exp },
        this.jwtSecret
      );
      res.setHeader(
        'Set-Cookie',
        `markus_token=${token}; HttpOnly; SameSite=Strict; Path=/; Max-Age=${7 * 24 * 3600}`
      );
      this.json(res, 200, {
        user: {
          id: userRow.id,
          name: userRow.name,
          email: userRow.email,
          role: userRow.role,
          orgId: userRow.orgId,
          avatarUrl: userRow.avatarUrl ?? undefined,
        },
        needsOnboarding: isFirstLogin,
      });
      return;
    }

    if (path === '/api/auth/logout' && req.method === 'POST') {
      res.setHeader('Set-Cookie', 'markus_token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0');
      this.json(res, 200, { ok: true });
      return;
    }

    if (path === '/api/auth/me' && req.method === 'GET') {
      const authUser = await this.getAuthUser(req);
      if (!authUser) {
        this.json(res, 401, { error: 'Unauthorized' });
        return;
      }
      if (!this.authEnabled) {
        this.json(res, 200, {
          user: { id: 'anonymous', name: 'Admin', role: 'owner', orgId: 'default' },
        });
        return;
      }
      const userRow = this.storage ? await this.storage.userRepo.findById(authUser.userId) : null;
      if (!userRow) {
        this.json(res, 401, { error: 'User not found' });
        return;
      }
      this.json(res, 200, {
        user: {
          id: userRow.id,
          name: userRow.name,
          email: userRow.email,
          role: userRow.role,
          orgId: userRow.orgId,
          avatarUrl: userRow.avatarUrl ?? undefined,
        },
      });
      return;
    }

    if (path === '/api/auth/change-password' && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      if (!this.storage) {
        this.json(res, 503, { error: 'Storage not available' });
        return;
      }
      const body = await this.readBody(req);
      const currentPassword = (body['currentPassword'] as string) ?? '';
      const newPassword = (body['newPassword'] as string) ?? '';
      if (!newPassword || newPassword.length < 6) {
        this.json(res, 400, { error: 'New password must be at least 6 characters' });
        return;
      }
      const userRow = await this.storage.userRepo.findById(authUser.userId);
      if (!userRow) {
        this.json(res, 404, { error: 'User not found' });
        return;
      }
      // If they already have a password, verify current one (skip for first-time setup where hash is null/empty)
      if (userRow.passwordHash && currentPassword) {
        const valid = await verifyPassword(currentPassword, userRow.passwordHash);
        if (!valid) {
          this.json(res, 401, { error: 'Current password is incorrect' });
          return;
        }
      }
      const newHash = await hashPassword(newPassword);
      await this.storage.userRepo.updatePassword(authUser.userId, newHash);
      // Re-issue token so session stays valid
      const exp = Math.floor(Date.now() / 1000) + 7 * 24 * 3600;
      const token = await signToken(
        { userId: userRow.id, orgId: userRow.orgId, role: userRow.role, exp },
        this.jwtSecret
      );
      res.setHeader(
        'Set-Cookie',
        `markus_token=${token}; HttpOnly; SameSite=Strict; Path=/; Max-Age=${7 * 24 * 3600}`
      );
      this.json(res, 200, { ok: true });
      return;
    }

    if (path === '/api/auth/setup' && req.method === 'POST') {
      if (!this.storage) {
        this.json(res, 503, { error: 'Storage not available' });
        return;
      }
      const body = await this.readBody(req);
      const token = (body['token'] as string) ?? '';
      const password = (body['password'] as string) ?? '';
      if (!token || !password) {
        this.json(res, 400, { error: 'Token and password are required' });
        return;
      }
      if (password.length < 6) {
        this.json(res, 400, { error: 'Password must be at least 6 characters' });
        return;
      }
      const userRow = this.storage.userRepo.findByInviteToken(token);
      if (!userRow) {
        this.json(res, 400, { error: 'Invalid or expired invite link' });
        return;
      }
      if (userRow.inviteExpiresAt && new Date(userRow.inviteExpiresAt) < new Date()) {
        this.json(res, 400, { error: 'Invite link has expired' });
        return;
      }
      const hash = await hashPassword(password);
      await this.storage.userRepo.updatePassword(userRow.id as string, hash);
      this.storage.userRepo.clearInviteToken(userRow.id as string);
      await this.storage.userRepo.updateLastLogin(userRow.id as string);
      const exp = Math.floor(Date.now() / 1000) + 7 * 24 * 3600;
      const jwtToken = await signToken(
        { userId: userRow.id, orgId: userRow.orgId, role: userRow.role, exp },
        this.jwtSecret
      );
      res.setHeader(
        'Set-Cookie',
        `markus_token=${jwtToken}; HttpOnly; SameSite=Strict; Path=/; Max-Age=${7 * 24 * 3600}`
      );

      this.json(res, 200, { ok: true, email: userRow.email });
      return;
    }

    if (path === '/api/auth/invite-info' && req.method === 'GET') {
      if (!this.storage) {
        this.json(res, 503, { error: 'Storage not available' });
        return;
      }
      const token = url.searchParams.get('token') ?? '';
      if (!token) {
        this.json(res, 400, { error: 'Token is required' });
        return;
      }
      const userRow = this.storage.userRepo.findByInviteToken(token);
      if (!userRow) {
        this.json(res, 400, { error: 'Invalid or expired invite link' });
        return;
      }
      if (userRow.inviteExpiresAt && new Date(userRow.inviteExpiresAt) < new Date()) {
        this.json(res, 400, { error: 'Invite link has expired' });
        return;
      }
      this.json(res, 200, { name: userRow.name, email: userRow.email });
      return;
    }

    if (path === '/api/auth/profile' && req.method === 'PUT') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      if (!this.storage) {
        this.json(res, 503, { error: 'Storage not available' });
        return;
      }
      const body = await this.readBody(req);
      const name = (body['name'] as string)?.trim();
      const email = (body['email'] as string)?.trim().toLowerCase();
      if (!name) {
        this.json(res, 400, { error: 'Name is required' });
        return;
      }
      if (email) {
        const existing = this.storage.userRepo.findByEmail(email);
        if (existing && existing.id !== authUser.userId) {
          this.json(res, 409, { error: 'Email already in use' });
          return;
        }
      }
      const updated = this.storage.userRepo.updateProfile(authUser.userId, {
        name,
        ...(email ? { email } : {}),
      });
      if (!updated) {
        this.json(res, 404, { error: 'User not found' });
        return;
      }
      // Update in-memory human user as well
      const human = this.orgService.getHumanUser(authUser.userId);
      if (human) {
        human.name = name;
        if (email) human.email = email;
      }
      // Re-issue token with updated info
      const exp = Math.floor(Date.now() / 1000) + 7 * 24 * 3600;
      const token = await signToken(
        { userId: updated.id, orgId: updated.orgId, role: updated.role, exp },
        this.jwtSecret
      );
      res.setHeader(
        'Set-Cookie',
        `markus_token=${token}; HttpOnly; SameSite=Strict; Path=/; Max-Age=${7 * 24 * 3600}`
      );
      this.json(res, 200, {
        user: {
          id: updated.id,
          name: updated.name,
          email: updated.email,
          role: updated.role,
          orgId: updated.orgId,
          avatarUrl: updated.avatarUrl ?? undefined,
        },
      });
      return;
    }

    // ── Avatar upload / serve ────────────────────────────────────────────
    if (path === '/api/avatars/upload' && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const body = await this.readBody(req);
      const targetType = (body['type'] as string) ?? 'user';
      const targetId = (body['id'] as string) ?? authUser.userId;
      const imageData = body['image'] as string;
      if (!imageData) {
        this.json(res, 400, { error: 'image is required (base64 data URL)' });
        return;
      }
      const match = imageData.match(/^data:image\/(png|jpeg|jpg|webp|gif);base64,(.+)$/);
      if (!match) {
        this.json(res, 400, { error: 'Invalid image format. Must be a base64 data URL (png/jpeg/webp/gif)' });
        return;
      }
      const ext = match[1] === 'jpeg' ? 'jpg' : match[1]!;
      const buf = Buffer.from(match[2]!, 'base64');
      if (buf.length > 2 * 1024 * 1024) {
        this.json(res, 400, { error: 'Image too large (max 2MB)' });
        return;
      }
      const avatarDir = join(homedir(), '.markus', 'avatars');
      mkdirSync(avatarDir, { recursive: true });
      const filename = `${targetType}_${targetId}.${ext}`;
      writeFileSync(join(avatarDir, filename), buf);
      const avatarUrl = `/api/avatars/${filename}`;
      if (targetType === 'user' && this.storage) {
        this.storage.userRepo.updateAvatarUrl(targetId, avatarUrl);
        const human = this.orgService.getHumanUser(targetId);
        if (human) (human as any).avatarUrl = avatarUrl;
      } else if (targetType === 'agent' && this.storage) {
        this.storage.agentRepo.updateAvatarUrl(targetId, avatarUrl);
      }
      this.json(res, 200, { avatarUrl });
      return;
    }

    if (path.startsWith('/api/avatars/') && req.method === 'GET') {
      const filename = decodeURIComponent(path.split('/')[3] ?? '');
      if (!filename || filename.includes('..')) {
        this.json(res, 400, { error: 'Invalid filename' });
        return;
      }
      const filePath = join(homedir(), '.markus', 'avatars', filename);
      if (existsSync(filePath) && statSync(filePath).isFile()) {
        this.serveStaticFile(res, filePath);
      } else {
        this.json(res, 404, { error: 'Avatar not found' });
      }
      return;
    }

    // ── File uploads (generic storage) ────────────────────────────────────
    if (path === '/api/uploads' && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      if (!this.fileStorage) {
        this.json(res, 503, { error: 'File storage not configured' });
        return;
      }
      const body = await this.readBody(req);
      const files = body['files'] as Array<{ dataUrl: string; name: string }> | undefined;
      if (!Array.isArray(files) || files.length === 0) {
        this.json(res, 400, { error: 'files[] is required (array of { dataUrl, name })' });
        return;
      }
      if (files.length > 10) {
        this.json(res, 400, { error: 'Too many files (max 10 per request)' });
        return;
      }
      const prefix = typeof body['prefix'] === 'string' ? body['prefix'] : undefined;
      const results: Array<{ url: string; key: string; name: string }> = [];
      for (const file of files) {
        if (!file.dataUrl || typeof file.dataUrl !== 'string') continue;
        const m = file.dataUrl.match(/^data:([^;]+);base64,(.+)$/);
        if (!m) continue;
        const contentType = m[1]!;
        const buf = Buffer.from(m[2]!, 'base64');
        if (buf.length > 10 * 1024 * 1024) continue;
        const result = await this.fileStorage.upload(buf, {
          name: file.name || 'file',
          contentType,
          prefix,
        });
        results.push({ ...result, name: file.name });
      }
      this.json(res, 200, { files: results });
      return;
    }

    if (path.startsWith('/api/uploads/') && req.method === 'GET') {
      if (!this.fileStorage) {
        this.json(res, 404, { error: 'File storage not configured' });
        return;
      }
      const key = decodeURIComponent(path.slice('/api/uploads/'.length));
      if (!key || key.includes('..')) {
        this.json(res, 400, { error: 'Invalid key' });
        return;
      }
      const filePath = this.fileStorage.resolve(key);
      if (existsSync(filePath) && statSync(filePath).isFile()) {
        this.serveStaticFile(res, filePath);
      } else {
        this.json(res, 404, { error: 'File not found' });
      }
      return;
    }

    // ── Chat sessions ──────────────────────────────────────────────────────
    if (path === '/api/sessions/has-any' && req.method === 'GET') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      if (!this.storage) {
        this.json(res, 200, { hasAny: false });
        return;
      }
      const hasAny = this.storage.chatSessionRepo.hasAnySessions(authUser.userId);
      this.json(res, 200, { hasAny });
      return;
    }

    if (path.match(/^\/api\/agents\/[^/]+\/sessions$/) && req.method === 'GET') {
      const authUser = await this.getAuthUser(req);
      const agentId = path.split('/')[3]!;
      if (!this.storage) {
        this.json(res, 200, { sessions: [] });
        return;
      }
      const limit = parseInt(url.searchParams.get('limit') ?? '20');
      const sessions = await this.storage.chatSessionRepo.getSessionsByAgent(agentId, limit, authUser?.userId);
      this.json(res, 200, { sessions });
      return;
    }

    if (path.match(/^\/api\/sessions\/[^/]+\/messages$/) && req.method === 'GET') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const sessionId = path.split('/')[3]!;
      if (!this.storage) {
        this.json(res, 200, { messages: [], hasMore: false });
        return;
      }
      const session = this.storage.chatSessionRepo.getSession(sessionId);
      if (session && session.userId && session.userId !== authUser.userId) {
        const isAdminOrOwner = authUser.role === 'owner' || authUser.role === 'admin';
        if (!isAdminOrOwner) {
          this.json(res, 403, { error: 'Access denied: this session belongs to another user' });
          return;
        }
      }
      const limit = parseInt(url.searchParams.get('limit') ?? '50');
      const before = url.searchParams.get('before') ?? undefined;
      const result = await this.storage.chatSessionRepo.getMessages(sessionId, limit, before);
      this.json(res, 200, result);
      return;
    }

    if (path.match(/^\/api\/sessions\/[^/]+$/) && req.method === 'DELETE') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const sessionId = path.split('/')[3]!;
      if (this.storage) await this.storage.chatSessionRepo.deleteSession(sessionId);
      this.json(res, 204, {});
      return;
    }

    // ── Channel messages ───────────────────────────────────────────────────
    if (path.match(/^\/api\/channels\/[^/]+\/messages$/) && req.method === 'GET') {
      const channel = decodeURIComponent(path.split('/')[3]!);
      if (!this.storage) {
        this.json(res, 200, { messages: [], hasMore: false });
        return;
      }
      const limit = parseInt(url.searchParams.get('limit') ?? '50');
      const before = url.searchParams.get('before') ?? undefined;
      const result = await this.storage.channelMessageRepo.getMessages(channel, limit, before);
      this.json(res, 200, result);
      return;
    }

    if (path.match(/^\/api\/channels\/[^/]+\/messages$/) && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const channel = decodeURIComponent(path.split('/')[3]!);
      const body = await this.readBody(req);
      const text = body['text'] as string;
      const resolvedIdentity = this.orgService.resolveHumanIdentity(authUser.userId);
      const senderId = authUser.userId;
      const senderName = resolvedIdentity?.name ?? (body['senderName'] as string) ?? 'You';
      const mentions = (body['mentions'] as string[]) ?? [];
      const targetAgentId = body['targetAgentId'] as string | undefined;
      const replyToId = body['replyToId'] as string | undefined;
      const orgId = (body['orgId'] as string) ?? 'default';

      // Persist user message
      let userMsg: ChannelMsg | undefined;
      if (this.storage) {
        userMsg = await this.storage.channelMessageRepo.append({
          orgId,
          channel,
          senderId,
          senderType: 'human',
          senderName,
          text,
          mentions,
          replyToId,
        });
      }

      // Broadcast unread update for channel message
      if (userMsg) {
        this.ws.broadcastUnreadUpdate(`channel:${channel}`, userMsg.id);
      }

      // DM / personal-notepad channels never route to agents
      const humanOnly = (body['humanOnly'] as boolean) === true;
      const isHumanChannel = humanOnly || channel.startsWith('notes:') || channel.startsWith('dm:');

      // Build channel context — give agents enough history to understand the full discussion
      const buildChannelContext = async (): Promise<Array<{ role: string; content: string }>> => {
        if (!this.storage) return [];
        try {
          const recent = await this.storage.channelMessageRepo.getMessages(channel, CHANNEL_CONTEXT_MESSAGES);
          return (recent.messages ?? []).map((m: ChannelMsg) => ({
            role: m.senderType === 'agent' ? 'assistant' : 'user',
            content: m.senderType === 'agent'
              ? stripInternalBlocks(m.text)
              : `[${m.senderName}]: ${m.text}`,
          }));
        } catch {
          return [];
        }
      };

      // Resolve reply context if the user is replying to a specific message
      let replyToAgentName: string | undefined;
      let replyToText: string | undefined;
      if (replyToId && this.storage) {
        try {
          const original = this.storage.channelMessageRepo.getMessageById(replyToId);
          if (original) {
            replyToAgentName = original.senderName;
            replyToText = original.text.slice(0, 200);
          }
        } catch { /* best-effort */ }
      }

      // Enrich userMsg with denormalized reply fields for the frontend
      const enrichedUserMsg = userMsg ? {
        ...userMsg,
        ...(replyToAgentName ? { replyToSender: replyToAgentName, replyToText } : {}),
      } : null;

      // Broadcast user message to other members so they see it in real time
      if (enrichedUserMsg) {
        let targetIds: string[] = [];
        if (channel.startsWith('group:')) {
          targetIds = this.resolveChannelHumanIds(channel).filter(id => id !== senderId);
        } else if (channel.startsWith('dm:')) {
          targetIds = channel.slice(3).split(':').filter(id => id !== senderId);
        }
        if (targetIds.length > 0) {
          this.ws.sendToUsers(targetIds, {
            type: 'chat:message',
            payload: {
              channel,
              senderId,
              senderType: 'human',
              senderName,
              text,
              id: enrichedUserMsg.id,
            },
            timestamp: new Date().toISOString(),
          });

          // Create notification bell entry for each recipient
          if (this.hitlService) {
            const preview = text.length > 80 ? text.slice(0, 80) + '…' : text;
            if (channel.startsWith('dm:')) {
              for (const tid of targetIds) {
                this.hitlService.notify({
                  targetUserId: tid,
                  type: 'direct_message',
                  title: senderName,
                  body: preview,
                  priority: 'normal',
                  actionType: 'navigate',
                  actionTarget: JSON.stringify({ path: `/team?dm=${senderId}` }),
                  metadata: { senderId, senderName, channel },
                });
              }
            } else if (channel.startsWith('group:')) {
              const gcName = this.resolveChannelName(channel);
              for (const tid of targetIds) {
                this.hitlService.notify({
                  targetUserId: tid,
                  type: 'group_message',
                  title: `${senderName} in ${gcName}`,
                  body: preview,
                  priority: 'low',
                  actionType: 'navigate',
                  actionTarget: JSON.stringify({ path: `/team?channel=${encodeURIComponent(channel)}` }),
                  metadata: { senderId, senderName, channel, groupName: gcName },
                });
              }
            }
          }
        }
      }

      // ── Group chat broadcast: all team members respond independently ──
      if (!isHumanChannel && channel.startsWith('group:')) {
        let allAgentIds: string[];
        if (channel.startsWith('group:custom:')) {
          allAgentIds = this.storage?.groupChatRepo?.getAgentMemberIds(channel) ?? [];
        } else {
          const teamId = channel.replace(/^group:/, '');
          const team = this.orgService.getTeam(teamId);
          allAgentIds = team?.memberAgentIds ?? [];
        }

        if (allAgentIds.length === 0) {
          this.json(res, 200, { userMessage: enrichedUserMsg, agentMessage: null });
          return;
        }

        const channelContext = await buildChannelContext();
        const senderInfo = this.orgService.resolveHumanIdentity(senderId);
        const agentManager = this.orgService.getAgentManager();

        const broadcastOpts = {
          mentionedNames: mentions.length > 0 ? mentions : undefined,
          replyToAgentName,
          replyToText,
        };

        // Prepare chain context so agent replies can trigger agent-to-agent chains
        const roundId = `round_${Date.now()}`;
        const respondedAgents = new Set<string>();
        const chainCtx = { roundId, depth: 0, respondedAgents, allAgentIds };

        // Fire-and-forget: each agent processes the message independently
        for (const agentId of allAgentIds) {
          void this.processGroupChatReply(
            agentId, text, senderId, senderInfo, channel, orgId, channelContext, agentManager, allAgentIds.length, broadcastOpts, chainCtx,
          );
        }

        // Return immediately with only the user message
        this.json(res, 200, { userMessage: enrichedUserMsg, agentMessage: null });
        return;
      }

      // ── Single-agent routing (@mention, non-group, or fallback) ──
      let routedAgentId: string | null | undefined = null;
      if (!isHumanChannel) {
        if (targetAgentId) {
          routedAgentId = targetAgentId;
        } else if (channel.startsWith('group:')) {
          // @mention already handled above; this is a fallback for custom group chats
          routedAgentId = this.orgService.routeMessage(orgId, { text });
        } else {
          routedAgentId = this.orgService.routeMessage(orgId, { text });
        }
      }
      if (!routedAgentId) {
        this.json(res, 200, { userMessage: enrichedUserMsg, agentMessage: null });
        return;
      }
      const agent = this.orgService.getAgentManager().getAgent(routedAgentId);
      const senderInfo = this.orgService.resolveHumanIdentity(senderId);

      const channelContext = await buildChannelContext();

      let reply: string;
      const toolEvents: Array<{ tool: string; status: 'done' | 'error'; arguments?: unknown; result?: string; durationMs?: number }> = [];
      try {
        reply = await agent.sendMessage(text, senderId, senderInfo, {
          sourceType: 'human_chat',
          channelContext,
          toolEventCollector: toolEvents,
        });
      } catch (err) {
        const raw = String(err);
        let detail = raw;
        const jsonMatch = raw.match(/\{[\s\S]*\}/);
        if (jsonMatch) {
          try {
            const parsed = JSON.parse(jsonMatch[0]) as {
              error?: { message?: string };
              message?: string;
            };
            detail = parsed.error?.message ?? parsed.message ?? raw;
          } catch {
            /* keep raw */
          }
        }

        // Persist error as a channel message so it survives page reloads
        let errorMsg: ChannelMsg | undefined;
        if (this.storage) {
          try {
            errorMsg = await this.storage.channelMessageRepo.append({
              orgId,
              channel,
              senderId: routedAgentId,
              senderType: 'system',
              senderName: 'System',
              text: `⚠ AI service error: ${detail.slice(0, 500)}`,
              mentions: [],
            });
          } catch (e) {
            log.warn('Failed to persist channel error message', { error: String(e) });
          }
        }

        const statusCode = raw.includes('402')
          ? 402
          : raw.includes('401')
            ? 401
            : raw.includes('429')
              ? 429
              : 502;
        this.json(res, statusCode, {
          userMessage: enrichedUserMsg,
          agentMessage: errorMsg ?? null,
          error: detail,
        });
        return;
      }

      // Separate clean text from internal process data
      const { thinking, clean: cleanReply } = extractThinkBlocks(reply);
      const metadata: Record<string, unknown> = {};
      if (thinking.length > 0) metadata['thinking'] = thinking;
      if (toolEvents.length > 0) metadata['toolCalls'] = toolEvents;

      // Persist agent reply
      let agentMsg: ChannelMsg | undefined;
      if (this.storage) {
        agentMsg = await this.storage.channelMessageRepo.append({
          orgId,
          channel,
          senderId: routedAgentId,
          senderType: 'agent',
          senderName: agent.config.name,
          text: cleanReply,
          mentions: [],
          metadata: Object.keys(metadata).length > 0 ? metadata as any : undefined,
        });
        void this.persistChatTurn(routedAgentId, text, reply, senderId);
      }

      // No WS broadcast here — the HTTP response delivers the agentMessage directly
      // to the requesting client. WS broadcast is only needed for group chat async replies.
      this.json(res, 200, {
        userMessage: enrichedUserMsg,
        agentMessage: agentMsg ?? {
          id: `tmp_${Date.now()}`,
          channel,
          senderId: routedAgentId,
          senderType: 'agent',
          senderName: agent.config.name,
          text: cleanReply,
          mentions: [],
          metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
          createdAt: new Date(),
        },
      });
      return;
    }

    // Agents
    if (path === '/api/agents' && req.method === 'GET') {
      let agents = this.orgService.getAgentManager().listAgents() as Array<Record<string, unknown>>;
      // Merge avatarUrl from storage
      if (this.storage) {
        const dbAgents = this.storage.agentRepo.listAll();
        const avatarMap = new Map(dbAgents.filter((a: any) => a.avatarUrl).map((a: any) => [a.id, a.avatarUrl]));
        if (avatarMap.size > 0) {
          agents = agents.map(a => {
            const av = avatarMap.get(a.id as string);
            return av ? { ...a, avatarUrl: av } : a;
          });
        }
      }
      if (this.gateway) {
        const extRegs = this.gateway.listRegistrations();
        const disconnectedIds = new Set(
          extRegs.filter(r => !r.connected && r.markusAgentId).map(r => r.markusAgentId!)
        );
        agents = agents.map(a =>
          disconnectedIds.has(a.id as string) ? { ...a, status: 'offline' } : a
        );
        this.json(res, 200, { agents });
      } else {
        this.json(res, 200, { agents });
      }
      return;
    }

    if (path === '/api/agents' && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const body = await this.readBody(req);
      const agentName = body['name'] as string;
      const roleName = body['roleName'] as string;
      if (!agentName?.trim()) {
        this.json(res, 400, { error: 'name is required' });
        return;
      }
      if (agentName.length > 100) {
        this.json(res, 400, { error: 'name must be 100 characters or fewer' });
        return;
      }
      const orgId = (body['orgId'] as string) ?? 'default';

      // Sanitize: strip HTML tags from name to prevent XSS
      const sanitizedName = stripHtmlTags(agentName);
      if (sanitizedName !== agentName) {
        log.warn('XSS sanitization applied to agent name', { original: agentName, sanitized: sanitizedName });
      }

      const agent = await this.orgService.hireAgent({
        name: sanitizedName,
        roleName: roleName?.trim() || undefined,
        orgId,
        teamId: body['teamId'] as string | undefined,
        skills: body['skills'] as string[] | undefined,
        agentRole: body['agentRole'] as 'manager' | 'worker' | undefined,
        tools: body['tools'] as AgentToolHandler[] | undefined,
      });
      this.auditService?.record({
        orgId,
        type: 'agent_hire',
        action: 'hire_agent',
        detail: `Agent "${agent.config.name}" hired`,
        userId: authUser?.userId,
        agentId: agent.id,
        success: true,
        metadata: { roleName: agent.role.name, teamId: body['teamId'] },
      });
      this.json(res, 201, {
        agent: {
          id: agent.id,
          name: agent.config.name,
          role: agent.role.name,
          agentRole: agent.config.agentRole,
          status: agent.getState().status,
          skills: agent.config.skills ?? [],
        },
      });
      return;
    }

    if (path.match(/^\/api\/agents\/[^/]+\/(start|stop|daily-report|a2a|message)$/) && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const parts = path.split('/');
      const agentId = parts[3];
      const action = parts[4];

      if (action === 'start') {
        await this.orgService.getAgentManager().startAgent(agentId!);
        this.ws.broadcastAgentUpdate(agentId!, 'idle');
        this.json(res, 200, { status: 'started' });
        return;
      }
      if (action === 'stop') {
        await this.orgService.getAgentManager().stopAgent(agentId!);
        this.ws.broadcastAgentUpdate(agentId!, 'offline');
        this.json(res, 200, { status: 'stopped' });
        return;
      }
      if (action === 'daily-report') {
        const agent = this.orgService.getAgentManager().getAgent(agentId!);
        const report = await agent.generateDailyReport();
        this.json(res, 200, { agentId: agentId!, report });
        return;
      }
      if (action === 'a2a') {
        const body = await this.readBody(req);
        const fromAgentId = body['fromAgentId'] as string;
        const messageText = body['message'] as string;
        const targetAgent = this.orgService.getAgentManager().getAgent(agentId!);
        const fromAgent = this.orgService.getAgentManager().getAgent(fromAgentId);
        const reply = await targetAgent.sendMessage(messageText, fromAgentId, {
          name: fromAgent.config.name,
          role: fromAgent.config.agentRole ?? 'worker',
        }, { sourceType: 'a2a_message', waitForReply: true });
        this.json(res, 200, { from: fromAgentId, to: agentId, reply });
        return;
      }
      if (action === 'message') {
        const body = await this.readBody(req);
        const stream = body['stream'] as boolean | undefined;
        const senderId = authUser.userId;
        const sessionId = body['sessionId'] as string | undefined ?? undefined;
        const images = (body['images'] as string[] | undefined)?.filter(Boolean);
        const fileNames = (body['fileNames'] as string[] | undefined)?.filter(Boolean);
        const isRetry = body['isRetry'] as boolean | undefined;
        const isResume = body['isResume'] as boolean | undefined;
        const baseSenderInfo = this.orgService.resolveHumanIdentity(senderId);
        const isFirstConversation = this.storage
          ? !this.storage.chatSessionRepo.hasAnySessions(senderId)
          : false;
        const senderInfo = baseSenderInfo
          ? { ...baseSenderInfo, isFirstConversation }
          : undefined;
        const agent = this.orgService.getAgentManager().getAgent(agentId!);
        this.ws.broadcastAgentUpdate(agentId!, 'working');

        if (!sessionId) {
          agent.startNewSession();
        } else if (this.storage) {
          // Restore agent memory context from DB session history so the agent
          // has full conversation context when replying to an existing chat.
          try {
            if (isRetry) {
              this.storage.chatSessionRepo.deleteLastExchange(sessionId);
            }
            // Resume: keep the existing assistant message in DB and memory so
            // the LLM can see its previous partial response and continue.
            const histResult = await this.storage.chatSessionRepo.getMessages(sessionId, 200);
            agent.restoreSessionFromHistory(
              sessionId,
              histResult.messages.map((m: { role: string; content: string }) => ({ role: m.role, content: m.content })),
              { isRetry: !!isRetry },
            );
          } catch (err) {
            log.warn('Failed to restore session history, starting fresh', { sessionId, error: String(err) });
            agent.startNewSession();
          }
        }

        const userText = body['text'] as string;

        // Wrap persistUserMessage to bind DB session → memory session on first message
        const bindingPersist = async (
          aId: string, text: string, sId?: string, imgs?: string[], sessId?: string,
        ): Promise<string | null> => {
          const dbSessId = await this.persistUserMessage(aId, text, sId, imgs, sessId);
          if (dbSessId && !sessId) {
            agent.bindDbSession(dbSessId);
          }
          return dbSessId;
        };

        if (stream) {
          const sseHandler = new SSEHandler({
            agentId: agentId!,
            agent,
            userText,
            images,
            fileNames,
            senderId,
            senderInfo,
            sessionId,
            wsBroadcaster: this.ws,
            persistUserMessage: bindingPersist,
            persistAssistantMessage: this.persistAssistantMessage.bind(this),
            executionStreamRepo: this.storage?.executionStreamRepo,
            isResume,
          });

          await sseHandler.handle(res);
        } else {
          const userMsgPersisted = await bindingPersist(agentId!, userText, senderId, images, sessionId);
          const toolEvents: Array<{ tool: string; status: 'done' | 'error'; arguments?: unknown; result?: string; durationMs?: number }> = [];
          let reply: string;
          try {
            reply = await agent.sendMessage(userText, senderId, senderInfo, { images, fileNames, toolEventCollector: toolEvents });
          } catch (err) {
            const errText = `⚠ AI service error: ${String(err).slice(0, 500)}`;
            void this.persistAssistantMessage(
              userMsgPersisted, agentId!, errText, 0, { isError: true },
            );
            throw err;
          }
          this.json(res, 200, { reply, sessionId: userMsgPersisted });
          const { thinking, clean: cleanReply } = extractThinkBlocks(reply);
          const segments: Array<Record<string, unknown>> = [];
          if (thinking.length > 0) segments.push({ type: 'text', content: '', thinking: thinking.join('\n\n') });
          for (const te of toolEvents) {
            segments.push({ type: 'tool', tool: te.tool, status: te.status, arguments: te.arguments, result: te.result, durationMs: te.durationMs });
          }
          if (segments.length > 0) segments.push({ type: 'text', content: cleanReply });
          const meta = segments.length > 0 ? { segments } : undefined;
          void this.persistAssistantMessage(
            userMsgPersisted,
            agentId!,
            reply,
            agent.getState().tokensUsedToday,
            meta
          );
        }

        const _st = agent.getState();
        this.ws.broadcastAgentUpdate(agentId!, _st.status, { lastError: _st.lastError, lastErrorAt: _st.lastErrorAt, currentActivity: _st.currentActivity });
        return;
      }
    }

    if (path.match(/^\/api\/agents\/[^/]+$/) && req.method === 'DELETE') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const agentId = path.split('/')[3]!;

      // Validate agent exists before deletion
      try {
        this.orgService.getAgentManager().getAgent(agentId);
      } catch {
        this.json(res, 404, { error: 'Agent not found' });
        return;
      }

      if (this.orgService.isProtectedAgent(agentId)) {
        this.json(res, 403, { error: 'The Secretary agent is a protected system agent and cannot be deleted.' });
        return;
      }
      const agentExists = this.orgService.getAgentManager().listAgents().some(a => a.id === agentId);
      if (!agentExists) {
        this.json(res, 404, { error: 'Agent not found' });
        return;
      }
      if (this.gateway) {
        const extReg = this.gateway.listRegistrations().find(r => r.markusAgentId === agentId);
        if (extReg) {
          await this.gateway.unregister(extReg.externalAgentId, extReg.orgId);
        }
      }
      const purgeFiles = url.searchParams.get('purgeFiles') === 'true';
      await this.orgService.fireAgent(agentId, { purgeFiles });
      this.auditService?.record({
        orgId: 'default',
        type: 'agent_fire',
        action: 'fire_agent',
        detail: `Agent ${agentId} removed`,
        userId: authUser?.userId,
        agentId,
        success: true,
        metadata: { purgeFiles },
      });
      this.json(res, 200, { deleted: true, purgedFiles: purgeFiles });
      return;
    }

    // ── Group Chats ──────────────────────────────────────────────────────────────
    if (path === '/api/group-chats' && req.method === 'GET') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const orgId = url.searchParams.get('orgId') ?? 'default';
      const userId = authUser.userId;
      const isAdmin = authUser.role === 'owner' || authUser.role === 'admin';
      const teams = this.orgService.listTeamsWithMembers(orgId);
      const teamChats = teams.map(t => ({
        id: `group:${t.id}`,
        name: t.name,
        type: 'team' as const,
        teamId: t.id,
        memberCount: t.members.length,
        channelKey: `group:${t.id}`,
      }));
      const customChats = this.storage?.groupChatRepo
        ? (isAdmin
            ? this.storage.groupChatRepo.list(orgId)
            : this.storage.groupChatRepo.listByMember(orgId, userId)
          ).map((c: any) => ({
            id: c.id,
            name: c.name,
            type: 'custom' as const,
            creatorId: c.creatorId,
            creatorName: c.creatorName,
            memberCount: c.memberCount,
            channelKey: c.channelKey,
          }))
        : [];
      this.json(res, 200, { chats: [...teamChats, ...customChats] });
      return;
    }

    if (path === '/api/group-chats' && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const body = await this.readBody(req);
      const name = body['name'] as string;
      const orgId = (body['orgId'] as string) ?? 'default';
      const creatorId = body['creatorId'] as string;
      const creatorName = body['creatorName'] as string;
      const memberIds = body['memberIds'] as string[] | undefined;
      const memberTypes = body['memberTypes'] as Record<string, string> | undefined;
      const memberNames = body['memberNames'] as Record<string, string> | undefined;
      if (!name) {
        this.json(res, 400, { error: 'name is required' });
        return;
      }
      if (!this.storage?.groupChatRepo) {
        this.json(res, 500, { error: 'Storage not initialized' });
        return;
      }
      const members = (memberIds ?? []).map(id => {
        const mType = (memberTypes?.[id] ?? 'agent') as 'human' | 'agent';
        let mName = memberNames?.[id] ?? id;
        if (mType === 'agent') {
          try { mName = this.orgService.getAgentManager().getAgent(id)?.config?.name ?? mName; } catch { /* */ }
        }
        return { id, type: mType, name: mName };
      });
      // Auto-add creator as a human member if not already included
      if (creatorId && !members.some(m => m.id === creatorId)) {
        members.unshift({ id: creatorId, type: 'human', name: creatorName ?? 'Unknown' });
      }
      const gc = this.storage.groupChatRepo.create({ orgId, name, creatorId, creatorName, members });
      this.ws?.broadcast({
        type: 'chat:group_created',
        payload: { chatId: gc.channelKey, name, creatorId, creatorName },
        timestamp: new Date().toISOString(),
      });
      this.json(res, 201, {
        chat: {
          id: gc.id, name: gc.name, type: 'custom', creatorId: gc.creatorId,
          creatorName: gc.creatorName, channelKey: gc.channelKey, memberCount: gc.members.length,
          members: gc.members.map((m: { memberId: string; memberName: string; memberType: string }) => ({ id: m.memberId, name: m.memberName, type: m.memberType })),
        },
      });
      return;
    }

    // GET /api/group-chats/:id — get group chat details
    if (path.match(/^\/api\/group-chats\/[^/]+$/) && req.method === 'GET') {
      const gcId = path.split('/').pop()!;
      if (!this.storage?.groupChatRepo) { this.json(res, 500, { error: 'Storage not initialized' }); return; }
      const gc = this.storage.groupChatRepo.getById(gcId);
      if (!gc) { this.json(res, 404, { error: 'Group chat not found' }); return; }
      this.json(res, 200, {
        chat: {
          id: gc.id, name: gc.name, type: 'custom', channelKey: gc.channelKey,
          creatorId: gc.creatorId, creatorName: gc.creatorName,
          members: gc.members.map((m: any) => ({ id: m.memberId, name: m.memberName, type: m.memberType })),
        },
      });
      return;
    }

    // PATCH /api/group-chats/:id — update group chat name
    if (path.match(/^\/api\/group-chats\/[^/]+$/) && req.method === 'PATCH') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const gcId = path.split('/').pop()!;
      if (!this.storage?.groupChatRepo) { this.json(res, 500, { error: 'Storage not initialized' }); return; }
      const body = await this.readBody(req);
      const newName = body['name'] as string;
      if (newName) this.storage.groupChatRepo.updateName(gcId, newName);
      this.json(res, 200, { ok: true });
      return;
    }

    // DELETE /api/group-chats/:id — delete group chat
    if (path.match(/^\/api\/group-chats\/[^/]+$/) && req.method === 'DELETE') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const gcId = path.split('/').pop()!;
      if (!this.storage?.groupChatRepo) { this.json(res, 500, { error: 'Storage not initialized' }); return; }
      this.storage.groupChatRepo.delete(gcId);
      this.ws?.broadcast({ type: 'chat:group_deleted', payload: { chatId: gcId }, timestamp: new Date().toISOString() });
      this.json(res, 200, { ok: true });
      return;
    }

    // POST /api/group-chats/:id/members — add member
    if (path.match(/^\/api\/group-chats\/[^/]+\/members$/) && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const gcId = path.split('/')[3]!;
      if (!this.storage?.groupChatRepo) { this.json(res, 500, { error: 'Storage not initialized' }); return; }
      const body = await this.readBody(req);
      const memberId = body['memberId'] as string;
      const memberType = (body['memberType'] as 'human' | 'agent') ?? 'agent';
      let memberName = body['memberName'] as string ?? memberId;
      if (memberType === 'agent') {
        try { memberName = this.orgService.getAgentManager().getAgent(memberId)?.config?.name ?? memberName; } catch { /* */ }
      }
      this.storage.groupChatRepo.addMember(gcId, memberId, memberType, memberName);
      this.ws?.broadcast({ type: 'chat:group_updated', payload: { chatId: gcId }, timestamp: new Date().toISOString() });
      this.json(res, 200, { ok: true });
      return;
    }

    // DELETE /api/group-chats/:id/members/:memberId — remove member
    if (path.match(/^\/api\/group-chats\/[^/]+\/members\/[^/]+$/) && req.method === 'DELETE') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const parts = path.split('/');
      const gcId = parts[3]!;
      const memberId = parts[5]!;
      if (!this.storage?.groupChatRepo) { this.json(res, 500, { error: 'Storage not initialized' }); return; }
      this.storage.groupChatRepo.removeMember(gcId, memberId);
      this.ws?.broadcast({ type: 'chat:group_updated', payload: { chatId: gcId }, timestamp: new Date().toISOString() });
      this.json(res, 200, { ok: true });
      return;
    }

    // Teams
    if (path === '/api/teams' && req.method === 'GET') {
      const orgId = url.searchParams.get('orgId') ?? 'default';
      const teams = this.orgService.listTeamsWithMembers(orgId);
      const ungrouped = this.orgService.listUngroupedMembers(orgId);
      const allMembers = [
        ...teams.flatMap(t => (t.members as unknown as Record<string, unknown>[])),
        ...(ungrouped as unknown as Record<string, unknown>[]),
      ];
      const agentIds = [...new Set(allMembers.filter(m => m.type === 'agent').map(m => m.id as string))];
      const userIds = [...new Set(allMembers.filter(m => m.type !== 'agent').map(m => m.id as string))];

      const avatarMap = new Map<string, string>();
      if (this.storage?.agentRepo) {
        for (const id of agentIds) {
          const a = this.storage.agentRepo.findById(id);
          if (a?.avatarUrl) avatarMap.set(id, a.avatarUrl);
        }
      }
      if (this.storage?.userRepo) {
        for (const id of userIds) {
          const u = this.storage.userRepo.findById(id);
          if (u?.avatarUrl) avatarMap.set(id, u.avatarUrl);
        }
      }

      const enrichMember = (m: Record<string, unknown>) => {
        const av = avatarMap.get(m.id as string);
        return av ? { ...m, avatarUrl: av } : m;
      };
      const enrichedTeams = teams.map(t => ({ ...t, members: (t.members as unknown as Record<string, unknown>[]).map(enrichMember) }));
      const enrichedUngrouped = (ungrouped as unknown as Record<string, unknown>[]).map(enrichMember);
      this.json(res, 200, { teams: enrichedTeams, ungrouped: enrichedUngrouped });
      return;
    }

    if (path === '/api/teams' && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      if (authUser.role !== 'owner' && authUser.role !== 'admin') {
        this.json(res, 403, { error: 'Insufficient permissions' });
        return;
      }
      const body = await this.readBody(req);
      const orgId = (body['orgId'] as string) ?? authUser.orgId ?? 'default';
      const name = body['name'] as string;
      if (!name) {
        this.json(res, 400, { error: 'name is required' });
        return;
      }
      const team = await this.orgService.createTeam(
        orgId,
        name,
        body['description'] as string | undefined
      );
      // Notify Chat page so the new team appears as a group chat
      this.ws?.broadcast({
        type: 'chat:group_created',
        payload: {
          chatId: `group:${team.id}`,
          name: team.name,
          creatorId: authUser.userId,
          creatorName: '',
        },
        timestamp: new Date().toISOString(),
      });
      this.json(res, 201, { team });
      return;
    }

    if (path.match(/^\/api\/teams\/[^/]+$/) && req.method === 'PATCH') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      if (authUser.role !== 'owner' && authUser.role !== 'admin') {
        this.json(res, 403, { error: 'Insufficient permissions' });
        return;
      }
      const teamId = path.split('/')[3]!;
      const body = await this.readBody(req);
      const team = await this.orgService.updateTeam(teamId, {
        name: body['name'] as string | undefined,
        description: body['description'] as string | undefined,
        managerId: body['managerId'] as string | undefined,
        managerType: body['managerType'] as 'human' | 'agent' | undefined,
      });
      this.json(res, 200, { team });
      return;
    }

    if (path.match(/^\/api\/teams\/[^/]+$/) && req.method === 'DELETE') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      if (authUser.role !== 'owner' && authUser.role !== 'admin') {
        this.json(res, 403, { error: 'Insufficient permissions' });
        return;
      }
      const teamId = path.split('/')[3]!;
      const deleteMembers = url.searchParams.get('deleteMembers') === 'true';
      const purgeFiles = url.searchParams.get('purgeFiles') === 'true';
      await this.orgService.deleteTeam(teamId, deleteMembers, { purgeFiles });
      this.json(res, 200, { deleted: true, purgedFiles: purgeFiles });
      return;
    }

    if (path.match(/^\/api\/teams\/[^/]+\/members$/) && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      if (authUser.role !== 'owner' && authUser.role !== 'admin') {
        this.json(res, 403, { error: 'Insufficient permissions' });
        return;
      }
      const teamId = path.split('/')[3]!;
      const body = await this.readBody(req);
      const memberId = body['memberId'] as string;
      const memberType = body['memberType'] as 'human' | 'agent';
      if (!memberId || !memberType) {
        this.json(res, 400, { error: 'memberId and memberType are required' });
        return;
      }
      this.orgService.addMemberToTeam(teamId, memberId, memberType);
      this.ws.broadcastTeamUpdate(teamId, { action: 'member-added', memberId });
      this.json(res, 200, { ok: true });
      return;
    }

    if (path.match(/^\/api\/teams\/[^/]+\/members\/[^/]+$/) && req.method === 'DELETE') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      if (authUser.role !== 'owner' && authUser.role !== 'admin') {
        this.json(res, 403, { error: 'Insufficient permissions' });
        return;
      }
      const parts = path.split('/');
      const teamId = parts[3]!;
      const memberId = parts[5]!;
      this.orgService.removeMemberFromTeam(teamId, memberId);
      this.ws.broadcastTeamUpdate(teamId, { action: 'member-removed', memberId });
      this.json(res, 200, { ok: true });
      return;
    }

    // Team batch start
    if (path.match(/^\/api\/teams\/[^/]+\/start$/) && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      if (authUser.role !== 'owner' && authUser.role !== 'admin') {
        this.json(res, 403, { error: 'Insufficient permissions' });
        return;
      }
      const teamId = path.split('/')[3]!;
      try {
        const result = await this.orgService.startTeamAgents(teamId);
        this.json(res, 200, result);
      } catch (err) {
        this.json(res, 404, { error: String(err) });
      }
      return;
    }

    // Team batch stop
    if (path.match(/^\/api\/teams\/[^/]+\/stop$/) && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      if (authUser.role !== 'owner' && authUser.role !== 'admin') {
        this.json(res, 403, { error: 'Insufficient permissions' });
        return;
      }
      const teamId = path.split('/')[3]!;
      try {
        const result = await this.orgService.stopTeamAgents(teamId);
        this.json(res, 200, result);
      } catch (err) {
        this.json(res, 404, { error: String(err) });
      }
      return;
    }

    // Team batch pause
    if (path.match(/^\/api\/teams\/[^/]+\/pause$/) && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      if (authUser.role !== 'owner' && authUser.role !== 'admin') {
        this.json(res, 403, { error: 'Insufficient permissions' });
        return;
      }
      const teamId = path.split('/')[3]!;
      const body = await this.readBody(req);
      try {
        const result = this.orgService.pauseTeamAgents(teamId, body['reason'] as string | undefined);
        this.json(res, 200, result);
      } catch (err) {
        this.json(res, 404, { error: String(err) });
      }
      return;
    }

    // Team batch resume
    if (path.match(/^\/api\/teams\/[^/]+\/resume$/) && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      if (authUser.role !== 'owner' && authUser.role !== 'admin') {
        this.json(res, 403, { error: 'Insufficient permissions' });
        return;
      }
      const teamId = path.split('/')[3]!;
      try {
        const result = this.orgService.resumeTeamAgents(teamId);
        this.json(res, 200, result);
      } catch (err) {
        this.json(res, 404, { error: String(err) });
      }
      return;
    }

    // Team agent status
    if (path.match(/^\/api\/teams\/[^/]+\/status$/) && req.method === 'GET') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const teamId = path.split('/')[3]!;
      const statuses = this.orgService.getTeamAgentStatuses(teamId);
      this.json(res, 200, { agents: statuses });
      return;
    }

    // Team files (announcements, norms, etc.)
    if (path.match(/^\/api\/teams\/[^/]+\/files$/) && req.method === 'GET') {
      const teamId = path.split('/')[3]!;
      const dir = this.orgService.getTeamDataDir(teamId);
      if (!existsSync(dir)) {
        this.json(res, 200, { files: [] });
        return;
      }
      const files = readdirSync(dir).filter(f => f.endsWith('.md'));
      this.json(res, 200, { files });
      return;
    }

    if (path.match(/^\/api\/teams\/[^/]+\/files\/[^/]+$/) && req.method === 'GET') {
      const parts = path.split('/');
      const teamId = parts[3]!;
      const filename = decodeURIComponent(parts[5]!);
      if (filename.includes('..') || filename.includes('/')) {
        this.json(res, 400, { error: 'Invalid filename' });
        return;
      }
      const filePath = join(this.orgService.getTeamDataDir(teamId), filename);
      if (!existsSync(filePath)) {
        this.json(res, 404, { error: 'File not found' });
        return;
      }
      const content = readFileSync(filePath, 'utf-8');
      this.json(res, 200, { filename, content });
      return;
    }

    if (path.match(/^\/api\/teams\/[^/]+\/files\/[^/]+$/) && req.method === 'PUT') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const parts = path.split('/');
      const teamId = parts[3]!;
      const filename = decodeURIComponent(parts[5]!);
      if (filename.includes('..') || filename.includes('/')) {
        this.json(res, 400, { error: 'Invalid filename' });
        return;
      }
      const body = await this.readBody(req);
      const content = body['content'] as string | undefined;
      const dir = this.orgService.getTeamDataDir(teamId);
      mkdirSync(dir, { recursive: true });
      writeFileSync(join(dir, filename), content ?? '', 'utf-8');
      this.json(res, 200, { ok: true });
      return;
    }

    // Roles
    if (path === '/api/roles' && req.method === 'GET') {
      const roleNames = this.orgService.listAvailableRoles();
      const roles = roleNames.map(name => {
        try {
          const details = this.orgService.getRoleDetails(name);
          return {
            id: name,
            name,
            description: details.description ?? '',
            category: details.category ?? 'custom',
          };
        } catch {
          return { id: name, name, description: '', category: 'custom' };
        }
      });
      this.json(res, 200, { roles });
      return;
    }

    if (path.startsWith('/api/roles/') && req.method === 'GET') {
      const roleName = path.split('/')[3]!;
      const role = this.orgService.getRoleDetails(roleName);
      this.json(res, 200, { role });
      return;
    }

    // Tasks
    if (path === '/api/tasks' && req.method === 'GET') {
      const orgId = url.searchParams.get('orgId') ?? undefined;
      const status = url.searchParams.get('status') as TaskStatus | undefined;
      const assignedAgentId = url.searchParams.get('assignedAgentId') ?? undefined;
      const projectId = url.searchParams.get('projectId') ?? undefined;
      const requirementId = url.searchParams.get('requirementId') ?? undefined;
      const priority = url.searchParams.get('priority') as TaskPriority | undefined;
      const search = url.searchParams.get('search') ?? undefined;
      const sortBy = url.searchParams.get('sortBy') as TaskSortField | undefined;
      const sortOrder = url.searchParams.get('sortOrder') as SortOrder | undefined;
      const pageParam = url.searchParams.get('page');
      const pageSizeParam = url.searchParams.get('pageSize');
      const page = pageParam ? parseInt(pageParam, 10) : undefined;
      const pageSize = pageSizeParam ? parseInt(pageSizeParam, 10) : undefined;

      const result = this.taskService.queryTasks({
        orgId, status, assignedAgentId, projectId, requirementId,
        priority, search, sortBy, sortOrder, page, pageSize,
      });
      this.json(res, 200, result);
      return;
    }

    if (path === '/api/tasks/scheduled' && req.method === 'GET') {
      const tasks = this.taskService.listScheduledTasks();
      this.json(res, 200, { tasks });
      return;
    }

    if (path === '/api/tasks/deliverables' && req.method === 'GET') {
      const projectId = url.searchParams.get('projectId') ?? undefined;
      const all = this.taskService.listTasks({ projectId });
      const items = all
        .filter(t => t.deliverables && t.deliverables.length > 0)
        .map(t => ({
          taskId: t.id,
          taskTitle: t.title,
          taskStatus: t.status,
          projectId: t.projectId,
          requirementId: t.requirementId,
          assignedAgentId: t.assignedAgentId,
          updatedAt: t.updatedAt,
          deliverables: t.deliverables,
        }));
      this.json(res, 200, { items });
      return;
    }

    // ── Unified Deliverables CRUD ──────────────────────────────────────────

    if (path === '/api/deliverables' && req.method === 'GET') {
      if (!this.deliverableService) { this.json(res, 503, { error: 'Deliverable service not available' }); return; }
      const q = url.searchParams.get('q') ?? undefined;
      const projectId = url.searchParams.get('projectId') ?? undefined;
      const agentId = url.searchParams.get('agentId') ?? undefined;
      const taskId = url.searchParams.get('taskId') ?? undefined;
      const type = url.searchParams.get('type') as any ?? undefined;
      const status = url.searchParams.get('status') as any ?? undefined;
      const artifactType = url.searchParams.get('artifactType') as any ?? undefined;
      const offset = url.searchParams.get('offset') ? Number(url.searchParams.get('offset')) : undefined;
      const limit = url.searchParams.get('limit') ? Number(url.searchParams.get('limit')) : undefined;
      const { results, total } = this.deliverableService.search({ query: q, projectId, agentId, taskId, type, status, artifactType, offset, limit });
      this.json(res, 200, { results, total });
      return;
    }

    if (path === '/api/deliverables' && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      if (!this.deliverableService) { this.json(res, 503, { error: 'Deliverable service not available' }); return; }
      const body = await this.readBody(req);
      try {
        const d = await this.deliverableService.create({
          type: body['type'] as any,
          title: body['title'] as string,
          summary: body['summary'] as string,
          reference: body['reference'] as string,
          format: body['format'] as string | undefined,
          tags: body['tags'] as string[],
          taskId: body['taskId'] as string,
          agentId: body['agentId'] as string,
          projectId: body['projectId'] as string,
          requirementId: body['requirementId'] as string,
        });
        this.json(res, 201, { deliverable: d });
      } catch (err) {
        this.json(res, 500, { error: String(err) });
      }
      return;
    }

    if (path.match(/^\/api\/deliverables\/[^/]+$/) && req.method === 'GET') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      if (!this.deliverableService) { this.json(res, 503, { error: 'Deliverable service not available' }); return; }
      const delivId = path.split('/')[3]!;
      try {
        const d = await this.deliverableService.get(delivId);
        if (!d) { this.json(res, 404, { error: 'Deliverable not found' }); return; }
        this.json(res, 200, { deliverable: d });
      } catch (err) {
        this.json(res, 500, { error: String(err) });
      }
      return;
    }

    if (path.match(/^\/api\/deliverables\/[^/]+$/) && req.method === 'PUT') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      if (!this.deliverableService) { this.json(res, 503, { error: 'Deliverable service not available' }); return; }
      const delivId = path.split('/')[3]!;
      const body = await this.readBody(req);
      try {
        const d = await this.deliverableService.update(delivId, {
          title: body['title'] as string | undefined,
          summary: body['summary'] as string | undefined,
          reference: body['reference'] as string | undefined,
          format: body['format'] as string | undefined,
          tags: body['tags'] as string[] | undefined,
          status: body['status'] as any,
          type: body['type'] as any,
        });
        if (!d) { this.json(res, 404, { error: 'Deliverable not found' }); return; }
        this.json(res, 200, { deliverable: d });
      } catch (err) {
        this.json(res, 500, { error: String(err) });
      }
      return;
    }

    if (path.match(/^\/api\/deliverables\/[^/]+$/) && req.method === 'DELETE') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      if (!this.deliverableService) { this.json(res, 503, { error: 'Deliverable service not available' }); return; }
      const delivId = path.split('/')[3]!;
      await this.deliverableService.remove(delivId);
      this.json(res, 200, { status: 'removed' });
      return;
    }

    if (path === '/api/tasks/dashboard' && req.method === 'GET') {
      const orgId = url.searchParams.get('orgId') ?? undefined;
      const dashboard = this.taskService.getDashboard(orgId);
      this.json(res, 200, dashboard);
      return;
    }

    if (path.match(/^\/api\/tasks\/[^/]+$/) && req.method === 'GET') {
      const taskId = path.split('/')[3]!;
      const task = this.taskService.getTask(taskId);
      if (!task) {
        this.json(res, 404, { error: `Task not found: ${taskId}` });
        return;
      }
      this.json(res, 200, { task });
      return;
    }

    if (path === '/api/tasks' && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const body = await this.readBody(req);
      const assignedAgentId = (body['assignedAgentId'] as string | undefined)?.trim();
      const reviewerId = (body['reviewerId'] as string | undefined)?.trim();
      const reviewerType = (body['reviewerType'] as string | undefined) === 'human' ? 'human' as const : 'agent' as const;
      if (!assignedAgentId || !reviewerId) {
        this.json(res, 400, { error: 'assignedAgentId and reviewerId are required' });
        return;
      }
      const agentMgr = this.orgService.getAgentManager();
      if (!agentMgr.hasAgent(assignedAgentId)) {
        this.json(res, 400, { error: `Assigned agent not found: ${assignedAgentId}` });
        return;
      }
      if (reviewerType !== 'human' && !agentMgr.hasAgent(reviewerId)) {
        this.json(res, 400, { error: `Reviewer agent not found: ${reviewerId}` });
        return;
      }
      const scheduleRaw = body['scheduleConfig'] as Record<string, unknown> | undefined;
      let task: ReturnType<typeof this.taskService.createTask>;
      try {
        task = this.taskService.createTask({
          orgId: (body['orgId'] as string) ?? 'default',
          title: body['title'] as string,
          description: body['description'] as string,
          priority: body['priority'] as TaskPriority | undefined,
          assignedAgentId,
          reviewerId,
          reviewerType,
          projectId: body['projectId'] as string | undefined,
          blockedBy: Array.isArray(body['blockedBy']) ? body['blockedBy'] as string[] : undefined,
          requirementId: body['requirementId'] as string | undefined,
          createdBy: authUser?.userId ?? 'unknown',
          creatorRole: 'human',
          taskType: ((body['taskType'] as string | undefined) ?? 'standard') as 'standard' | 'scheduled',
          scheduleConfig: scheduleRaw ? {
            cron: scheduleRaw['cron'] as string | undefined,
            every: scheduleRaw['every'] as string | undefined,
            runAt: scheduleRaw['runAt'] as string | undefined,
            timezone: scheduleRaw['timezone'] as string | undefined,
            maxRuns: scheduleRaw['maxRuns'] as number | undefined,
          } : undefined,
        });
      } catch (e) {
        const msg = e instanceof Error ? e.message : String(e);
        let code = 'unknown';
        if (msg.includes('task cap reached') || msg.includes('active task cap')) code = 'task_limit_reached';
        else if (msg.includes('must reference an approved requirement')) code = 'requirement_required';
        else if (msg.includes('agent not found')) code = 'agent_not_found';
        this.json(res, 400, { error: msg, code });
        return;
      }
      this.auditService?.record({
        orgId: task.orgId,
        type: 'task_created',
        action: 'create_task',
        detail: `Task "${task.title}" created`,
        userId: authUser?.userId,
        agentId: task.assignedAgentId,
        taskId: task.id,
        projectId: task.projectId,
        success: true,
        metadata: { reviewerId: task.reviewerId, requirementId: task.requirementId },
      });
      this.json(res, 201, { task });
      return;
    }

    if (path.match(/^\/api\/tasks\/[^/]+$/) && req.method === 'PUT') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const taskId = path.split('/')[3]!;
      const body = await this.readBody(req);

      if (body['status']) {
        const task = this.taskService.updateTaskStatus(taskId, body['status'] as TaskStatus, authUser?.userId);
        this.json(res, 200, { task });
        return;
      }

      if ('assignedAgentId' in body) {
        const agentId = body['assignedAgentId'] as string | null;
        if (agentId) {
          const task = this.taskService.assignTask(taskId, agentId, authUser?.userId);
          this.json(res, 200, { task });
        } else {
          this.json(res, 400, { error: 'assignedAgentId is required — tasks must always have an assignee' });
        }
        return;
      }

      // General field update (title/description/priority/projectId/requirementId/blockedBy/reviewerId/reviewerType)
      if (
        body['title'] !== undefined ||
        body['description'] !== undefined ||
        body['priority'] !== undefined ||
        body['projectId'] !== undefined ||
        body['requirementId'] !== undefined ||
        body['blockedBy'] !== undefined ||
        body['reviewerId'] !== undefined ||
        body['reviewerType'] !== undefined
      ) {
        const task = this.taskService.updateTask(taskId, {
          title: body['title'] as string | undefined,
          description: body['description'] as string | undefined,
          priority: body['priority'] as TaskPriority | undefined,
          projectId: body['projectId'] !== undefined ? (body['projectId'] as string | null) : undefined,
          requirementId: body['requirementId'] !== undefined ? (body['requirementId'] as string | null) : undefined,
          blockedBy: Array.isArray(body['blockedBy']) ? body['blockedBy'] as string[] : undefined,
          reviewerId: body['reviewerId'] as string | undefined,
          reviewerType: body['reviewerType'] as 'agent' | 'human' | undefined,
        }, authUser?.userId);
        this.json(res, 200, { task });
        return;
      }

      this.json(res, 400, { error: 'Provide status, assignedAgentId, or task fields to update' });
      return;
    }

    // Task approve/reject — the only way to transition out of pending.
    // If the UI changed the assignee before approving, that's already on the task object.
    if (path.match(/^\/api\/tasks\/[^/]+\/approve$/) && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const taskId = path.split('/')[3]!;
      try {
        const body = await this.readBody(req).catch(() => ({} as Record<string, unknown>));
        const runNow = body['runNow'] === true;
        const task = this.taskService.approveTask(taskId, authUser?.userId, runNow);
        this.auditService?.record({
          orgId: task.orgId,
          type: 'task_approval_granted',
          action: 'approve_task',
          detail: `Task "${task.title}" approved`,
          userId: authUser?.userId,
          agentId: task.assignedAgentId,
          taskId: task.id,
          projectId: task.projectId,
          success: true,
        });
        this.json(res, 200, { task });
      } catch (err: unknown) {
        this.json(res, 400, { error: err instanceof Error ? err.message : String(err) });
      }
      return;
    }

    if (path.match(/^\/api\/tasks\/[^/]+\/reject$/) && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const taskId = path.split('/')[3]!;
      try {
        const task = this.taskService.rejectTask(taskId, authUser?.userId);
        this.auditService?.record({
          orgId: task.orgId,
          type: 'task_approval_rejected',
          action: 'reject_task',
          detail: `Task "${task.title}" rejected`,
          userId: authUser?.userId,
          agentId: task.assignedAgentId,
          taskId: task.id,
          projectId: task.projectId,
          success: true,
        });
        this.json(res, 200, { task });
      } catch (err: unknown) {
        this.json(res, 400, { error: err instanceof Error ? err.message : String(err) });
      }
      return;
    }

    if (path.match(/^\/api\/tasks\/[^/]+\/cancel$/) && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const taskId = path.split('/')[3]!;
      const body = await this.readBody(req);
      const cascade = body['cascade'] === true;
      try {
        const task = this.taskService.cancelTask(taskId, cascade, authUser?.userId, 'human');
        this.auditService?.record({
          orgId: task.orgId,
          type: 'task_cancelled',
          action: 'cancel_task',
          detail: `Task "${task.title}" cancelled`,
          userId: authUser?.userId,
          agentId: task.assignedAgentId,
          taskId: task.id,
          projectId: task.projectId,
          success: true,
          metadata: { cascade },
        });
        this.json(res, 200, { task });
      } catch (err) {
        this.json(res, 400, { error: String(err) });
      }
      return;
    }

    if (path.match(/^\/api\/tasks\/[^/]+\/dependents$/) && req.method === 'GET') {
      const taskId = path.split('/')[3]!;
      const count = this.taskService.getDependentTaskCount(taskId);
      this.json(res, 200, { count });
      return;
    }

    if (path.startsWith('/api/tasks/') && req.method === 'DELETE' && !path.includes('/subtasks/')) {
      this.json(res, 400, { error: 'Tasks cannot be deleted — use cancel instead to preserve audit trail' });
      return;
    }

    // Subtasks (embedded within a task)
    if (path.match(/^\/api\/tasks\/[^/]+\/subtasks$/) && req.method === 'GET') {
      const taskId = path.split('/')[3]!;
      const task = this.taskService.getTask(taskId);
      if (!task) { this.json(res, 404, { error: 'Task not found' }); return; }
      this.json(res, 200, { subtasks: task.subtasks });
      return;
    }

    if (path.match(/^\/api\/tasks\/[^/]+\/subtasks$/) && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const body = await this.readBody(req);
      const taskId = path.split('/')[3]!;
      const subtask = this.taskService.addSubtask(taskId, body['title'] as string);
      this.json(res, 201, { subtask });
      return;
    }

    // Complete/cancel a specific subtask
    const subtaskActionMatch = path.match(/^\/api\/tasks\/([^/]+)\/subtasks\/([^/]+)\/(complete|cancel)$/);
    if (subtaskActionMatch && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const taskId = subtaskActionMatch[1]!;
      const subtaskId = subtaskActionMatch[2]!;
      const action = subtaskActionMatch[3]!;
      const sub = action === 'complete'
        ? this.taskService.completeSubtask(taskId, subtaskId)
        : this.taskService.cancelSubtask(taskId, subtaskId);
      this.json(res, 200, { subtask: sub });
      return;
    }

    // Delete a specific subtask
    const subtaskDeleteMatch = path.match(/^\/api\/tasks\/([^/]+)\/subtasks\/([^/]+)$/);
    if (subtaskDeleteMatch && req.method === 'DELETE') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const taskId = subtaskDeleteMatch[1]!;
      const subtaskId = subtaskDeleteMatch[2]!;
      this.taskService.deleteSubtask(taskId, subtaskId);
      this.json(res, 200, { ok: true });
      return;
    }

    if (path === '/api/taskboard' && req.method === 'GET') {
      const orgId = url.searchParams.get('orgId') ?? 'default';
      const projectId = url.searchParams.get('projectId') ?? undefined;
      const board = this.taskService.getTaskBoard(orgId, { projectId });
      this.json(res, 200, { board });
      return;
    }

    // Comprehensive operations dashboard
    if (path === '/api/ops/dashboard' && req.method === 'GET') {
      const orgId = url.searchParams.get('orgId') ?? undefined;
      const period = (url.searchParams.get('period') ?? '24h') as '1h' | '24h' | '7d';
      const opsDashboard = this.buildOpsDashboard(orgId, period);
      this.json(res, 200, opsDashboard);
      return;
    }

    // Task execution: run a task with its assigned agent (fire-and-forget)
    if (path.match(/^\/api\/tasks\/[^/]+\/run$/) && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const taskId = path.split('/')[3]!;
      try {
        const task = this.taskService.getTask(taskId);
        if (!task) { this.json(res, 404, { error: 'Task not found' }); return; }
        if (task.status !== 'in_progress') {
          this.json(res, 400, { error: `Cannot run task in ${task.status} status — must be in_progress` });
          return;
        }
        await this.taskService.runTask(taskId);
        this.json(res, 202, { status: 'running', taskId });
      } catch (err) {
        this.json(res, 400, { error: String(err) });
      }
      return;
    }

    // Unified execution stream logs
    if (path === '/api/execution-logs' && req.method === 'GET') {
      const sourceType = url.searchParams.get('sourceType');
      const sourceId = url.searchParams.get('sourceId');
      if (!sourceType || !sourceId) {
        this.json(res, 400, { error: 'sourceType and sourceId required' });
        return;
      }
      if (!this.storage?.executionStreamRepo) {
        this.json(res, 200, { logs: [] });
        return;
      }
      try {
        const logs = this.storage.executionStreamRepo.getBySource(sourceType, sourceId);
        this.json(res, 200, { logs });
      } catch (err) {
        this.json(res, 500, { error: String(err) });
      }
      return;
    }

    // Task execution logs — rounds summary (lightweight metadata only)
    if (path.match(/^\/api\/tasks\/[^/]+\/logs\/summary$/) && req.method === 'GET') {
      const taskId = path.split('/')[3]!;
      if (!this.storage) {
        this.json(res, 200, { rounds: [] });
        return;
      }
      try {
        const rounds = this.storage.taskLogRepo.getRoundsSummary(taskId);
        this.json(res, 200, { rounds });
      } catch (err) {
        this.json(res, 500, { error: String(err) });
      }
      return;
    }

    // Task execution logs — optionally filtered by round
    if (path.match(/^\/api\/tasks\/[^/]+\/logs$/) && req.method === 'GET') {
      const taskId = path.split('/')[3]!;
      if (!this.storage) {
        this.json(res, 200, { logs: [] });
        return;
      }
      try {
        const roundParam = url.searchParams.get('round');
        const logs = roundParam
          ? this.storage.taskLogRepo.getByTaskRound(taskId, parseInt(roundParam, 10))
          : await this.storage.taskLogRepo.getByTask(taskId);
        this.json(res, 200, { logs });
      } catch (err) {
        this.json(res, 500, { error: String(err) });
      }
      return;
    }

    // Task comments — add a comment (text + optional image attachments + @mentions)
    if (path.match(/^\/api\/tasks\/[^/]+\/comments$/) && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const taskId = path.split('/')[3]!;
      try {
        const body = await this.readBody(req);
        const mentions = (body['mentions'] as string[] | undefined) ?? [];
        let resolvedAuthorName = (body['authorName'] as string | undefined);
        if (!resolvedAuthorName && authUser?.userId && this.storage?.userRepo) {
          const userRow = await this.storage.userRepo.findById(authUser.userId);
          resolvedAuthorName = userRow?.name;
        }
        const authorId = (body['authorId'] as string) ?? authUser?.userId ?? 'human';
        const authorName = resolvedAuthorName ?? 'User';
        // Single entry point: DB write + WS broadcast + inject into running task + agent notifications
        const result = await this.taskService.postTaskComment(
          taskId, authorId, authorName,
          body['content'] as string,
          mentions, undefined,
          { authorType: (body['authorType'] as string) ?? 'human', attachments: body['attachments'] as unknown[] | undefined, replyToId: body['replyTo'] as string | undefined },
        );
        this.json(res, 201, { comment: result.comment });
      } catch (err) {
        this.json(res, 500, { error: String(err) });
      }
      return;
    }

    // Task comments — list comments for a task
    if (path.match(/^\/api\/tasks\/[^/]+\/comments$/) && req.method === 'GET') {
      const taskId = path.split('/')[3]!;
      if (!this.storage?.taskCommentRepo) {
        this.json(res, 200, { comments: [] });
        return;
      }
      try {
        const comments = await this.storage.taskCommentRepo.getByTask(taskId);
        this.json(res, 200, { comments });
      } catch (err) {
        this.json(res, 500, { error: String(err) });
      }
      return;
    }

    // Task status history — list all status transitions for a task
    if (path.match(/^\/api\/tasks\/[^/]+\/history$/) && req.method === 'GET') {
      const taskId = path.split('/')[3]!;
      try {
        const history = this.taskService.getTaskStatusHistory(taskId);
        this.json(res, 200, { history });
      } catch (err) {
        this.json(res, 500, { error: String(err) });
      }
      return;
    }

    // Task pause — explicitly pause a running task
    if (path.match(/^\/api\/tasks\/[^/]+\/pause$/) && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const taskId = path.split('/')[3]!;
      try {
        this.taskService.pauseTask(taskId, authUser.userId, 'human');
        this.json(res, 200, { status: 'blocked' as TaskStatus, taskId });
      } catch (err) {
        this.json(res, 400, { error: String(err) });
      }
      return;
    }

    // Task resume — resume a paused (blocked) task.
    // Transition blocked → in_progress via updateTaskStatus; the auto-start
    // side effect in handleTransitionSideEffects will schedule runTask.
    if (path.match(/^\/api\/tasks\/[^/]+\/resume$/) && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const taskId = path.split('/')[3]!;
      try {
        this.taskService.resumeTask(taskId, authUser.userId, 'human');
        this.json(res, 202, { status: 'running', taskId });
      } catch (err) {
        this.json(res, 400, { error: String(err) });
      }
      return;
    }

    // Task retry fresh — discard previous execution, start clean
    if (path.match(/^\/api\/tasks\/[^/]+\/retry$/) && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const taskId = path.split('/')[3]!;
      try {
        const task = await this.taskService.retryTaskFresh(taskId);
        this.json(res, 202, { task });
      } catch (err) {
        this.json(res, 400, { error: String(err) });
      }
      return;
    }

    // Organizations
    if (path === '/api/orgs' && req.method === 'GET') {
      const orgs = this.orgService.listOrganizations();
      this.json(res, 200, { orgs });
      return;
    }

    if (path === '/api/orgs' && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const body = await this.readBody(req);
      const org = await this.orgService.createOrganization(
        body['name'] as string,
        (body['ownerId'] as string) ?? authUser.userId
      );
      this.json(res, 201, { org });
      return;
    }

    // Team export — read all team directory files
    if (path.match(/^\/api\/teams\/[^/]+\/export$/) && req.method === 'GET') {
      const teamId = path.split('/')[3]!;
      try {
        const team = this.orgService.getTeam(teamId);
        if (!team) { this.json(res, 404, { error: 'Team not found' }); return; }
        const teamDataDir = this.orgService.getTeamDataDir(teamId);
        const files: Record<string, string> = {};
        if (teamDataDir && existsSync(teamDataDir)) {
          for (const fname of readdirSync(teamDataDir)) {
            const fpath = join(teamDataDir, fname);
            try {
              files[fname] = readFileSync(fpath, 'utf-8');
            } catch { /* skip */ }
          }
        }

        // Include member agent role files under members/{slug}/
        const agentManager = this.orgService.getAgentManager();
        const roleFileNames = ['ROLE.md', 'POLICIES.md', 'CONTEXT.md', 'HEARTBEAT.md'];
        for (const agentId of team.memberAgentIds ?? []) {
          try {
            const agent = agentManager.getAgent(agentId);
            const roleDir = this.resolveAgentRoleDir(agent);
            if (!roleDir) continue;
            const slug = kebab(agent.config.name, agentId);
            for (const fname of roleFileNames) {
              const fpath = join(roleDir, fname);
              if (existsSync(fpath)) {
                try { files[`members/${slug}/${fname}`] = readFileSync(fpath, 'utf-8'); } catch { /* skip */ }
              }
            }
          } catch { /* agent may not exist */ }
        }

        this.json(res, 200, { files, team: { id: team.id, name: team.name, description: team.description } });
      } catch {
        this.json(res, 404, { error: `Team not found: ${teamId}` });
      }
      return;
    }

    // Skill files — read all files from a skill directory
    if (path.match(/^\/api\/skills\/[^/]+\/files$/) && req.method === 'GET') {
      const skillName = decodeURIComponent(path.split('/')[3]!);
      const skillDir = join(homedir(), '.markus', 'skills', skillName);
      const files: Record<string, string> = {};
      if (existsSync(skillDir)) {
        for (const fname of readdirSync(skillDir)) {
          const fpath = join(skillDir, fname);
          try {
            files[fname] = readFileSync(fpath, 'utf-8');
          } catch { /* skip */ }
        }
      }
      if (Object.keys(files).length === 0) {
        this.json(res, 404, { error: `Skill not found: ${skillName}` });
      } else {
        this.json(res, 200, { files });
      }
      return;
    }

    // Agent mind state — current attention, focus, mailbox snapshot
    if (path.match(/^\/api\/agents\/[^/]+\/mind$/) && req.method === 'GET') {
      const agentId = path.split('/')[3]!;
      try {
        const agent = this.orgService.getAgentManager().getAgent(agentId);
        this.json(res, 200, agent.getMindState());
      } catch {
        this.json(res, 404, { error: `Agent not found: ${agentId}` });
      }
      return;
    }

    // Agent mailbox — queued items + enriched history (decisions + activity)
    if (path.match(/^\/api\/agents\/[^/]+\/mailbox$/) && req.method === 'GET') {
      const agentId = path.split('/')[3]!;
      const limit = parseInt(url.searchParams.get('limit') ?? '50', 10);
      const offset = parseInt(url.searchParams.get('offset') ?? '0', 10);
      const status = url.searchParams.get('status') ?? undefined;
      const category = url.searchParams.get('category') ?? undefined;
      const sourceType = url.searchParams.get('sourceType') ?? undefined;
      try {
        const agent = this.orgService.getAgentManager().getAgent(agentId);
        const queued = agent.getMailbox().getQueuedItems();

        let sourceTypes: string[] | undefined;
        if (sourceType) {
          sourceTypes = sourceType.split(',').map(s => s.trim()).filter(Boolean);
        } else if (category) {
          const { MAILBOX_CATEGORIES } = await import('@markus/shared');
          const cat = MAILBOX_CATEGORIES[category as keyof typeof MAILBOX_CATEGORIES];
          if (cat) sourceTypes = cat.types;
        }

        let history: Array<Record<string, unknown>> = [];
        if (this.storage?.mailboxRepo) {
          const raw = this.storage.mailboxRepo.getHistory(agentId, { limit, offset, sourceTypes, status });
          const itemIds = raw.map((item: { id: string }) => item.id);

          const decisionsMap = this.storage?.decisionRepo
            ? this.storage.decisionRepo.getByMailboxItemIds(itemIds)
            : new Map<string, unknown[]>();
          const activitiesMap = this.storage?.activityRepo
            ? this.storage.activityRepo.getByMailboxItemIds(itemIds)
            : new Map<string, unknown>();

          history = raw.map((item: { id: string; [k: string]: unknown }) => {
            const enriched: Record<string, unknown> = { ...item };
            enriched.decisions = decisionsMap.get(item.id) ?? [];
            const act = activitiesMap.get(item.id) as Record<string, unknown> | undefined;
            enriched.activity = act ? {
              id: act.id,
              type: act.type,
              label: act.label,
              startedAt: act.startedAt,
              endedAt: act.endedAt,
              totalTokens: act.totalTokens,
              totalTools: act.totalTools,
              success: act.success,
            } : null;
            return enriched;
          });
        }

        const statusCounts = this.storage?.mailboxRepo?.getStatusCounts(agentId) ?? {};
        const sourceTypeCounts = this.storage?.mailboxRepo?.getSourceTypeCounts(agentId) ?? {};

        this.json(res, 200, {
          queued: queued.map(i => ({
            id: i.id,
            sourceType: i.sourceType,
            priority: i.priority,
            status: i.status,
            summary: i.payload.summary,
            queuedAt: i.queuedAt,
          })),
          queueDepth: queued.length,
          statusCounts,
          sourceTypeCounts,
          history,
        });
      } catch {
        this.json(res, 404, { error: `Agent not found: ${agentId}` });
      }
      return;
    }

    // Agent attention decisions — decision timeline
    if (path.match(/^\/api\/agents\/[^/]+\/decisions$/) && req.method === 'GET') {
      const agentId = path.split('/')[3]!;
      const limit = parseInt(url.searchParams.get('limit') ?? '50', 10);
      try {
        const agent = this.orgService.getAgentManager().getAgent(agentId);
        const recent = agent.getAttentionController().getRecentDecisions(limit);

        let persisted: unknown[] = [];
        if (this.storage?.decisionRepo) {
          persisted = this.storage.decisionRepo.getByAgent(agentId, limit);
        }

        this.json(res, 200, {
          recent,
          persisted,
        });
      } catch {
        this.json(res, 404, { error: `Agent not found: ${agentId}` });
      }
      return;
    }

    // Agent metrics — must be before the generic GET /api/agents/:id handler
    if (path.match(/^\/api\/agents\/[^/]+\/metrics$/) && req.method === 'GET') {
      const agentId = path.split('/')[3]!;
      const period = (url.searchParams.get('period') ?? '24h') as '1h' | '24h' | '7d';
      try {
        const agent = this.orgService.getAgentManager().getAgent(agentId);
        const metrics = agent.getMetrics(period);
        this.json(res, 200, metrics);
      } catch {
        this.json(res, 404, { error: `Agent not found: ${agentId}` });
      }
      return;
    }

    // Agent config update (PATCH)
    if (path.match(/^\/api\/agents\/[^/]+\/config$/) && req.method === 'PATCH') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const agentId = path.split('/')[3]!;
      try {
        const agent = this.orgService.getAgentManager().getAgent(agentId);
        const body = await this.readBody(req);
        const cfg = agent.config as unknown as Record<string, unknown>;
        if (body['name'] !== undefined) {
          const name = stripHtmlTags(body['name'] as string);
          if (name.length > 100) {
            this.json(res, 400, { error: 'name must be 100 characters or fewer' });
            return;
          }
          cfg.name = name;
        }
        if (body['agentRole'] !== undefined) cfg.agentRole = body['agentRole'];
        if (body['skills'] !== undefined) cfg.skills = body['skills'];
        if (body['llmConfig'] !== undefined) {
          const lc = body['llmConfig'] as Record<string, unknown>;
          cfg.llmConfig = { ...(cfg.llmConfig as Record<string, unknown>), ...lc };
        }
        if (body['heartbeatIntervalMs'] !== undefined)
          cfg.heartbeatIntervalMs = body['heartbeatIntervalMs'];

        // Persist config changes to DB
        if (this.storage) {
          try {
            await this.storage.agentRepo.updateConfig(agentId, {
              name: body['name'] !== undefined ? stripHtmlTags(body['name'] as string) : undefined,
              agentRole: body['agentRole'] as string | undefined,
              skills: body['skills'] as unknown,
              llmConfig: cfg.llmConfig,
              heartbeatIntervalMs: body['heartbeatIntervalMs'] as number | undefined,
            });
          } catch (persistErr) {
            log.warn('Failed to persist agent config to DB', { agentId, error: String(persistErr) });
          }
        }

        this.json(res, 200, { ok: true, config: agent.config });
      } catch {
        this.json(res, 404, { error: `Agent not found: ${agentId}` });
      }
      return;
    }

    // Agent memory summary
    if (path.match(/^\/api\/agents\/[^/]+\/memory$/) && req.method === 'GET') {
      const agentId = path.split('/')[3]!;
      try {
        const agent = this.orgService.getAgentManager().getAgent(agentId);
        const mem = agent.getMemory();
        const entries = mem.getEntries(undefined, 50);
        const sessions = mem.listSessions(agentId);
        const dailyLog = mem.getDailyLog();
        const recentDailyLogs = mem.getRecentDailyLogs(7);
        const longTermMemory = mem.getLongTermMemory();
        this.json(res, 200, {
          entries: entries.map(e => ({
            type: e.type,
            content: e.content,
            timestamp: e.timestamp,
            importance: (e as unknown as Record<string, unknown>).importance,
          })),
          sessions: sessions.map(s => ({
            id: s.id,
            agentId: s.agentId,
            messageCount: s.messages.length,
            createdAt:
              ((s as unknown as Record<string, unknown>).createdAt as string) ??
              new Date().toISOString(),
            updatedAt:
              ((s as unknown as Record<string, unknown>).updatedAt as string) ??
              new Date().toISOString(),
          })),
          dailyLog: dailyLog ?? null,
          recentDailyLogs: recentDailyLogs ?? null,
          longTermMemory: longTermMemory ?? null,
        });
      } catch {
        this.json(res, 404, { error: `Agent not found: ${agentId}` });
      }
      return;
    }

    // Agent memory session messages
    if (path.match(/^\/api\/agents\/[^/]+\/memory\/sessions\/[^/]+$/) && req.method === 'GET') {
      const parts = path.split('/');
      const agentId = parts[3]!;
      const sessionId = decodeURIComponent(parts[6]!);
      try {
        const agent = this.orgService.getAgentManager().getAgent(agentId);
        const session = agent.getMemory().getSession(sessionId);
        if (!session) {
          this.json(res, 404, { error: `Session not found: ${sessionId}` });
          return;
        }
        this.json(res, 200, {
          id: session.id,
          agentId: session.agentId,
          startedAt: session.startedAt,
          lastActivityAt: session.lastActivityAt,
          messages: session.messages.map(m => ({
            role: m.role,
            content: getTextContent(m.content),
            ...(m.toolCalls?.length ? {
              toolCalls: m.toolCalls.map(tc => ({
                id: tc.id,
                name: tc.name,
                arguments: JSON.stringify(tc.arguments),
              })),
            } : {}),
            ...(m.toolCallId ? { toolCallId: m.toolCallId } : {}),
          })),
        });
      } catch {
        this.json(res, 404, { error: `Agent not found: ${agentId}` });
      }
      return;
    }

    // Agent memory: update daily log
    if (path.match(/^\/api\/agents\/[^/]+\/memory\/daily$/) && req.method === 'PUT') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const agentId = path.split('/')[3]!;
      try {
        const agent = this.orgService.getAgentManager().getAgent(agentId);
        const body = await this.readBody(req);
        const content = (body['content'] as string) ?? '';
        agent.getMemory().writeDailyLog(agentId, content);
        this.json(res, 200, { ok: true });
      } catch {
        this.json(res, 404, { error: `Agent not found: ${agentId}` });
      }
      return;
    }

    // Agent memory: update long-term memory
    if (path.match(/^\/api\/agents\/[^/]+\/memory\/longterm$/) && req.method === 'PUT') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const agentId = path.split('/')[3]!;
      try {
        const agent = this.orgService.getAgentManager().getAgent(agentId);
        const body = await this.readBody(req);
        const key = (body['key'] as string) ?? '';
        const content = (body['content'] as string) ?? '';
        agent.getMemory().addLongTermMemory(key, content);
        this.json(res, 200, { ok: true });
      } catch {
        this.json(res, 404, { error: `Agent not found: ${agentId}` });
      }
      return;
    }

    // Agent role/prompt files: list
    if (path.match(/^\/api\/agents\/[^/]+\/files$/) && req.method === 'GET') {
      const agentId = path.split('/')[3]!;
      try {
        const agent = this.orgService.getAgentManager().getAgent(agentId);
        const roleDir = this.resolveAgentRoleDir(agent);
        if (!roleDir) {
          this.json(res, 404, { error: `Role directory not found for agent: ${agent.role.name}` });
          return;
        }
        const allowedNames = ['ROLE.md', 'HEARTBEAT.md', 'POLICIES.md', 'CONTEXT.md'];
        const files: Array<{ name: string; content: string }> = [];
        const filesMap: Record<string, string> = {};
        for (const name of allowedNames) {
          const filePath = join(roleDir, name);
          if (existsSync(filePath)) {
            const content = readFileSync(filePath, 'utf-8');
            files.push({ name, content });
            filesMap[name] = content;
          }
        }
        this.json(res, 200, { files, filesMap });
      } catch {
        this.json(res, 404, { error: `Agent not found: ${agentId}` });
      }
      return;
    }

    // Agent role/prompt files: update
    if (path.match(/^\/api\/agents\/[^/]+\/files\/[^/]+$/) && req.method === 'PUT') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const parts = path.split('/');
      const agentId = parts[3]!;
      const filename = decodeURIComponent(parts[5]!);
      try {
        const agent = this.orgService.getAgentManager().getAgent(agentId);
        const roleDir = this.resolveAgentRoleDir(agent);
        if (!roleDir) {
          this.json(res, 404, { error: `Role directory not found for agent: ${agent.role.name}` });
          return;
        }
        const allowedNames = ['ROLE.md', 'HEARTBEAT.md', 'POLICIES.md', 'CONTEXT.md'];
        if (!allowedNames.includes(filename)) {
          this.json(res, 400, { error: `Invalid filename. Allowed: ${allowedNames.join(', ')}` });
          return;
        }
        const body = await this.readBody(req);
        const content = (body['content'] as string) ?? '';
        writeFileSync(join(roleDir, filename), content, 'utf-8');
        this.json(res, 200, { ok: true });
      } catch {
        this.json(res, 404, { error: `Agent not found: ${agentId}` });
      }
      return;
    }

    // Agent system prompt: update (runtime only)
    if (path.match(/^\/api\/agents\/[^/]+\/system-prompt$/) && req.method === 'PUT') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const agentId = path.split('/')[3]!;
      try {
        const agent = this.orgService.getAgentManager().getAgent(agentId);
        const body = await this.readBody(req);
        const systemPrompt = (body['systemPrompt'] as string) ?? '';
        (agent.role as { systemPrompt: string }).systemPrompt = systemPrompt;
        this.json(res, 200, { ok: true });
      } catch {
        this.json(res, 404, { error: `Agent not found: ${agentId}` });
      }
      return;
    }

    // ─── Role Template Versioning & Sync ──────────────────────────────────

    // Check role update status for a single agent
    if (path.match(/^\/api\/agents\/[^/]+\/role-status$/) && req.method === 'GET') {
      const agentId = path.split('/')[3]!;
      try {
        const status = this.orgService.getAgentManager().checkRoleUpdate(agentId);
        this.json(res, 200, status);
      } catch {
        this.json(res, 404, { error: `Agent not found: ${agentId}` });
      }
      return;
    }

    // Get file-level diff between agent's role and template
    if (path.match(/^\/api\/agents\/[^/]+\/role-diff$/) && req.method === 'GET') {
      const agentId = path.split('/')[3]!;
      const url = new URL(req.url!, `http://${req.headers.host}`);
      const fileName = url.searchParams.get('file') || 'ROLE.md';
      try {
        const diff = this.orgService.getAgentManager().getRoleFileDiff(agentId, fileName);
        this.json(res, 200, diff);
      } catch {
        this.json(res, 404, { error: `Agent not found: ${agentId}` });
      }
      return;
    }

    // Sync agent's role files from template
    if (path.match(/^\/api\/agents\/[^/]+\/role-sync$/) && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const agentId = path.split('/')[3]!;
      try {
        const body = await this.readBody(req);
        const files = Array.isArray(body['files']) ? (body['files'] as string[]) : undefined;
        const result = this.orgService.getAgentManager().syncRoleFromTemplate(agentId, files);
        this.json(res, result.success ? 200 : 400, result);
      } catch {
        this.json(res, 404, { error: `Agent not found: ${agentId}` });
      }
      return;
    }

    // Smart sync: use LLM to merge template changes while preserving agent customizations
    if (path.match(/^\/api\/agents\/[^/]+\/role-smart-sync$/) && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const agentId = path.split('/')[3]!;
      if (!this.llmRouter) {
        this.json(res, 503, { error: 'LLM router not available for smart sync' });
        return;
      }
      try {
        const body = await this.readBody(req);
        const fileName = (body['file'] as string) ?? 'ROLE.md';
        const diff = this.orgService.getAgentManager().getRoleFileDiff(agentId, fileName);

        if (!diff.agentContent || !diff.templateContent) {
          this.json(res, 400, { error: 'Cannot smart sync: missing agent or template content' });
          return;
        }

        if (diff.agentContent === diff.templateContent) {
          this.json(res, 200, { success: true, mergedContent: diff.agentContent, explanation: 'Files are already identical.' });
          return;
        }

        const prompt = `You are a configuration merge assistant. You need to intelligently merge a template update into an agent's configuration file while preserving the agent's customizations.

AGENT'S CURRENT FILE (${fileName}):
\`\`\`
${diff.agentContent}
\`\`\`

UPDATED TEMPLATE FILE (${fileName}):
\`\`\`
${diff.templateContent}
\`\`\`

INSTRUCTIONS:
1. Identify what changed in the template compared to the agent's current file
2. Preserve the agent's custom modifications (things the user deliberately changed from the original template)
3. Incorporate valuable new additions from the template (new sections, improved wording, new capabilities)
4. If the agent removed something from the original template, keep it removed (respect user intent)
5. If the template added something new, include it
6. If both modified the same section, prefer the agent's version but incorporate template improvements where they don't conflict

Output ONLY two sections:
MERGED_CONTENT_START
(the merged file content here)
MERGED_CONTENT_END

EXPLANATION_START
(brief bullet points of what was kept, added, and why)
EXPLANATION_END`;

        const llmResponse = await this.llmRouter.chat({
          messages: [
            { role: 'system', content: 'You are a precise configuration merge tool. Output exactly the requested format.' },
            { role: 'user', content: prompt },
          ],
          maxTokens: 8192,
          temperature: 0.1,
        });

        const text = llmResponse.content;
        const mergedMatch = text.match(/MERGED_CONTENT_START\n([\s\S]*?)\nMERGED_CONTENT_END/);
        const explanationMatch = text.match(/EXPLANATION_START\n([\s\S]*?)\nEXPLANATION_END/);

        if (!mergedMatch) {
          this.json(res, 500, { error: 'LLM did not produce valid merged content', success: false });
          return;
        }

        this.json(res, 200, {
          success: true,
          mergedContent: mergedMatch[1]!.trim(),
          explanation: explanationMatch?.[1]?.trim() ?? 'Merge completed.',
        });
      } catch (err) {
        this.json(res, 500, { error: `Smart sync failed: ${String(err)}`, success: false });
      }
      return;
    }

    // Batch check: all agents' role update status
    if (path === '/api/agents/role-updates' && req.method === 'GET') {
      const results = this.orgService.getAgentManager().checkAllRoleUpdates();
      const stale = results.filter(r => r.hasTemplate && !r.isUpToDate);
      this.json(res, 200, { total: results.length, staleCount: stale.length, stale });
      return;
    }

    // Agent skills: add
    if (path.match(/^\/api\/agents\/[^/]+\/skills$/) && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const agentId = path.split('/')[3]!;
      try {
        const agent = this.orgService.getAgentManager().getAgent(agentId);
        const body = await this.readBody(req);
        const skillName = (body['skillName'] as string) ?? '';
        if (skillName && !agent.config.skills.includes(skillName)) {
          agent.config.skills.push(skillName);
          // Inject skill instructions into the running agent so it can use them immediately
          if (this.skillRegistry) {
            const skill = this.skillRegistry.get(skillName);
            if (skill?.manifest.instructions) {
              agent.injectSkillInstructions(skillName, skill.manifest.instructions);
            }
          }
          // When a building skill is added, register builder dynamic context so the
          // agent can see available skills/roles, just like the seeded builder agents.
          if (['agent-building', 'team-building', 'skill-building'].includes(skillName)) {
            agent.addDynamicContextProvider(
              () => this.orgService.buildBuilderDynamicContext(this.skillRegistry),
              'builder-context'
            );
          }
        }
        if (this.storage) {
          try { await this.storage.agentRepo.updateConfig(agentId, { skills: agent.config.skills }); }
          catch (e) { log.warn('Failed to persist skill assignment', { agentId, error: String(e) }); }
        }
        this.json(res, 200, { ok: true, skills: agent.getActiveSkillNames() });
      } catch {
        this.json(res, 404, { error: `Agent not found: ${agentId}` });
      }
      return;
    }

    // Agent skills: remove
    if (path.match(/^\/api\/agents\/[^/]+\/skills\/[^/]+$/) && req.method === 'DELETE') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const parts = path.split('/');
      const agentId = parts[3]!;
      const skillName = decodeURIComponent(parts[5]!);
      try {
        const agent = this.orgService.getAgentManager().getAgent(agentId);
        agent.config.skills = agent.config.skills.filter(s => s !== skillName);
        agent.deactivateSkill(skillName);
        if (this.storage) {
          try { await this.storage.agentRepo.updateConfig(agentId, { skills: agent.config.skills }); }
          catch (e) { log.warn('Failed to persist skill removal', { agentId, error: String(e) }); }
        }
        this.json(res, 200, { ok: true, skills: agent.getActiveSkillNames() });
      } catch {
        this.json(res, 404, { error: `Agent not found: ${agentId}` });
      }
      return;
    }

    // Agent tools: toggle enable/disable (skeleton - tools can't easily be toggled at runtime)
    if (path.match(/^\/api\/agents\/[^/]+\/tools\/[^/]+\/toggle$/) && req.method === 'POST') {
      const parts = path.split('/');
      const agentId = parts[3]!;
      const toolName = decodeURIComponent(parts[5]!);
      try {
        this.orgService.getAgentManager().getAgent(agentId);
        const body = await this.readBody(req);
        const enabled = (body['enabled'] as boolean) ?? true;
        void toolName;
        void enabled;
        this.json(res, 200, { ok: true });
      } catch {
        this.json(res, 404, { error: `Agent not found: ${agentId}` });
      }
      return;
    }

    // Agent activities — persistent history from SQLite (session-grouped)
    if (path.match(/^\/api\/agents\/[^/]+\/activities$/) && req.method === 'GET') {
      const agentId = path.split('/')[3]!;
      const typeFilter = url.searchParams.get('type') ?? undefined;
      const limit = parseInt(url.searchParams.get('limit') ?? '30', 10);
      const before = url.searchParams.get('before') ?? undefined;
      const taskIdFilter = url.searchParams.get('taskId') ?? undefined;
      try {
        if (this.storage?.activityRepo) {
          const activities = this.storage.activityRepo.queryActivities(agentId, { type: typeFilter, limit, before, taskId: taskIdFilter });
          this.json(res, 200, { activities });
        } else {
          this.json(res, 200, { activities: [] });
        }
      } catch (err) {
        this.json(res, 500, { error: `Failed to query activities: ${String(err)}` });
      }
      return;
    }

    // Agent recent activities — list summary of in-memory activities (live)
    if (path.match(/^\/api\/agents\/[^/]+\/recent-activities$/) && req.method === 'GET') {
      const agentId = path.split('/')[3]!;
      try {
        const agent = this.orgService.getAgentManager().getAgent(agentId);
        const activities = agent.getRecentActivities();
        this.json(res, 200, { activities });
      } catch {
        this.json(res, 404, { error: `Agent not found: ${agentId}` });
      }
      return;
    }

    // Agent activity logs — in-memory for live activities, SQLite for historical
    if (path.match(/^\/api\/agents\/[^/]+\/activity-logs$/) && req.method === 'GET') {
      const agentId = path.split('/')[3]!;
      const activityId = url.searchParams.get('activityId');
      if (!activityId) {
        this.json(res, 400, { error: 'activityId query parameter is required' });
        return;
      }
      try {
        const agent = this.orgService.getAgentManager().getAgent(agentId);
        const currentActivity = agent.getCurrentActivity();
        if (currentActivity?.id === activityId) {
          const logs = agent.getActivityLogs(activityId);
          this.json(res, 200, { logs, activity: currentActivity });
          return;
        }
      } catch { /* agent not found, try SQLite */ }

      if (this.storage?.activityRepo) {
        const activity = this.storage.activityRepo.getActivity(activityId);
        const logs = this.storage.activityRepo.getActivityLogs(activityId);
        this.json(res, 200, { logs, activity });
      } else {
        this.json(res, 200, { logs: [], activity: undefined });
      }
      return;
    }

    // Agent heartbeat info — enriched with last summary, next run estimate
    if (path.match(/^\/api\/agents\/[^/]+\/heartbeat$/) && req.method === 'GET') {
      const agentId = path.split('/')[3]!;
      try {
        const agent = this.orgService.getAgentManager().getAgent(agentId);
        const hb = (
          agent as unknown as { heartbeat: { getStatus(): { running: boolean; uptimeMs: number; intervalMs: number; initialDelayMs: number } } }
        ).heartbeat;
        const status = hb.getStatus();

        // Get last heartbeat summary from agent memory
        let lastSummary: string | undefined;
        let lastSummaryAt: string | undefined;
        try {
          const mem = agent.getMemory();
          const results = mem.search('heartbeat:summary');
          if (results.length > 0) {
            const latest = results[results.length - 1];
            lastSummary = latest?.content;
            lastSummaryAt = latest?.timestamp;
          }
        } catch { /* ok */ }

        const state = agent.getState();
        const lastHeartbeat = state.lastHeartbeat;

        // Estimate next heartbeat time
        let nextRunAt: string | undefined;
        if (status.running && status.intervalMs > 0 && lastHeartbeat) {
          const next = new Date(new Date(lastHeartbeat).getTime() + status.intervalMs);
          if (next.getTime() > Date.now()) nextRunAt = next.toISOString();
        } else if (status.running && status.intervalMs > 0) {
          const next = new Date(Date.now() + status.intervalMs - status.uptimeMs % status.intervalMs);
          nextRunAt = next.toISOString();
        }

        this.json(res, 200, {
          ...status,
          lastHeartbeat,
          lastSummary,
          lastSummaryAt,
          nextRunAt,
        });
      } catch {
        this.json(res, 404, { error: `Agent not found: ${agentId}` });
      }
      return;
    }

    // Manual heartbeat trigger
    if (path.match(/^\/api\/agents\/[^/]+\/heartbeat\/trigger$/) && req.method === 'POST') {
      const agentId = path.split('/')[3]!;
      try {
        const agent = this.orgService.getAgentManager().getAgent(agentId);
        const hb = (
          agent as unknown as { heartbeat: { trigger(): void; isRunning(): boolean } }
        ).heartbeat;
        if (!hb.isRunning()) {
          this.json(res, 400, { error: 'Heartbeat scheduler is not running' });
          return;
        }
        hb.trigger();
        this.json(res, 200, { status: 'triggered', message: 'Heartbeat triggered. Check activity logs for results.' });
      } catch {
        this.json(res, 404, { error: `Agent not found: ${agentId}` });
      }
      return;
    }

    // Agent detail (GET) — enriched with config, tools, heartbeat summary
    if (path.match(/^\/api\/agents\/[^/]+$/) && req.method === 'GET') {
      const agentId = path.split('/')[3]!;
      try {
        const agent = this.orgService.getAgentManager().getAgent(agentId);
        const state = agent.getState();
        const tools = (
          agent as unknown as { tools: Map<string, { name: string; description: string }> }
        ).tools;
        const toolList = [...tools.values()].map(t => ({
          name: t.name,
          description: t.description,
        }));
        const virtualToolNames = new Set(toolList.map(t => t.name));
        const VIRTUAL_TOOLS: Array<{ name: string; description: string }> = [
          { name: 'discover_tools', description: 'Discover and activate additional tools and skills available to this agent' },
          { name: 'notify_user', description: 'Send a notification to a human team member (appears in chat + notification bell)' },
          { name: 'request_user_approval', description: 'Request a decision or approval from a human (blocks until response)' },
          { name: 'recall_activity', description: 'Query your own execution history and past activity logs' },
        ];
        for (const vt of VIRTUAL_TOOLS) {
          if (!virtualToolNames.has(vt.name)) toolList.push(vt);
        }
        const hb = (
          agent as unknown as { heartbeat: { getStatus(): { running: boolean; uptimeMs: number; intervalMs: number; initialDelayMs: number } } }
        ).heartbeat;
        let heartbeatSummary: Record<string, unknown> = {};
        try {
          heartbeatSummary = hb.getStatus() as unknown as Record<string, unknown>;
        } catch {
          /* ok */
        }
        const storedAgent = this.storage?.agentRepo.findById(agentId);
        this.json(res, 200, {
          id: agent.id,
          name: agent.config.name,
          role: agent.role.name,
          roleDescription: agent.role.description,
          agentRole: agent.config.agentRole,
          avatarUrl: storedAgent?.avatarUrl ?? undefined,
          state,
          activeTaskCount: state.activeTaskCount,
          activeTaskIds: state.activeTaskIds,
          skills: agent.getActiveSkillNames(),
          availableSkills: this.skillRegistry?.list().map(s => ({
            name: s.name,
            description: s.description,
            category: s.category,
            builtIn: !!s.builtIn,
            alwaysOn: !!s.alwaysOn,
          })) ?? [],
          proficiency: agent.getSkillProficiency(),
          config: {
            llmConfig: agent.config.llmConfig,
            channels: agent.config.channels,
            heartbeatIntervalMs: agent.config.heartbeatIntervalMs,
            orgId: agent.config.orgId,
            teamId: agent.config.teamId,
            createdAt: agent.config.createdAt,
          },
          tools: toolList,
          heartbeat: heartbeatSummary,
        });
      } catch {
        this.json(res, 404, { error: `Agent not found: ${agentId}` });
      }
      return;
    }

    // ── Review Service ─────────────────────────────────────────────────────
    if (path === '/api/reviews' && req.method === 'POST') {
      if (!this.reviewService) {
        this.json(res, 503, { error: 'Review service not configured' });
        return;
      }
      const body = await this.readBody(req);
      const report = await this.reviewService.runReview({
        taskId: body['taskId'] as string | undefined,
        agentId: body['agentId'] as string | undefined,
        changedFiles: body['changedFiles'] as string[] | undefined,
        description: body['description'] as string | undefined,
      });
      this.json(res, 200, report);
      return;
    }

    if (path === '/api/reviews' && req.method === 'GET') {
      if (!this.reviewService) {
        this.json(res, 503, { error: 'Review service not configured' });
        return;
      }
      const taskId = url.searchParams.get('taskId');
      const limit = parseInt(url.searchParams.get('limit') ?? '20', 10);
      const reports = taskId
        ? this.reviewService.getReportsByTask(taskId)
        : this.reviewService.getRecentReports(limit);
      this.json(res, 200, { reports });
      return;
    }

    if (path.match(/^\/api\/reviews\/[^/]+$/) && req.method === 'GET') {
      if (!this.reviewService) {
        this.json(res, 503, { error: 'Review service not configured' });
        return;
      }
      const reviewId = path.split('/')[3]!;
      const report = this.reviewService.getReport(reviewId);
      if (!report) {
        this.json(res, 404, { error: 'Review not found' });
        return;
      }
      this.json(res, 200, report);
      return;
    }

    // ── External Agent Gateway ──────────────────────────────────────────────
    if (path === '/api/gateway/info' && req.method === 'GET') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      if (authUser.role !== 'admin') {
        this.json(res, 403, { error: 'Admin access required' });
        return;
      }
      const host = req.headers['host'] ?? `localhost:${this.port}`;
      const proto = req.headers['x-forwarded-proto'] ?? 'http';
      const gatewayUrl = `${proto}://${host}/api/gateway`;
      const secret = this.gatewaySecret ?? '';
      const masked = secret.length > 8
        ? secret.slice(0, 4) + '*'.repeat(secret.length - 8) + secret.slice(-4)
        : secret;
      this.json(res, 200, {
        gatewayUrl,
        orgId: 'default',
        orgSecret: masked,
        orgSecretFull: secret,
        enabled: !!this.gateway,
      });
      return;
    }

    if (path === '/api/gateway/register' && req.method === 'POST') {
      if (!this.gateway) {
        this.json(res, 503, { error: 'Gateway not configured' });
        return;
      }
      const body = await this.readBody(req);
      try {
        const reg = await this.gateway.register({
          externalAgentId: body['agentId'] as string,
          agentName: body['agentName'] as string,
          orgId: body['orgId'] as string,
          capabilities: (body['capabilities'] as string[]) ?? [],
          platform: body['platform'] as string | undefined,
          platformConfig: body['platformConfig'] as string | undefined,
          agentCardUrl: body['agentCardUrl'] as string | undefined,
          openClawConfig: body['openClawConfig'] as string | undefined,
        });
        this.json(res, 201, reg);
      } catch (err) {
        if (err instanceof GatewayError) {
          this.json(res, err.statusCode, { error: err.message });
          return;
        }
        this.json(res, 500, { error: String(err) });
      }
      return;
    }

    if (path === '/api/gateway/auth' && req.method === 'POST') {
      if (!this.gateway) {
        this.json(res, 503, { error: 'Gateway not configured' });
        return;
      }
      const body = await this.readBody(req);
      try {
        const result = this.gateway.authenticate({
          externalAgentId: body['agentId'] as string,
          orgId: body['orgId'] as string,
          secret: body['secret'] as string,
        });
        this.json(res, 200, result);
      } catch (err) {
        if (err instanceof GatewayError) {
          this.json(res, err.statusCode, { error: err.message });
          return;
        }
        this.json(res, 500, { error: String(err) });
      }
      return;
    }

    if (path === '/api/gateway/message' && req.method === 'POST') {
      if (!this.gateway) {
        this.json(res, 503, { error: 'Gateway not configured' });
        return;
      }
      const authHeader = req.headers['authorization'];
      if (!authHeader?.startsWith('Bearer ')) {
        this.json(res, 401, { error: 'Missing Bearer token' });
        return;
      }
      try {
        const token = this.gateway.verifyToken(authHeader.slice(7));
        const body = await this.readBody(req);
        const result = await this.gateway.routeMessage(token, {
          type: body['type'] as 'task' | 'status' | 'heartbeat',
          content: body['content'] as string,
          metadata: body['metadata'] as Record<string, unknown> | undefined,
        });
        this.json(res, 200, result);
      } catch (err) {
        if (err instanceof GatewayError) {
          this.json(res, err.statusCode, { error: err.message });
          return;
        }
        this.json(res, 500, { error: String(err) });
      }
      return;
    }

    if (path === '/api/gateway/status' && req.method === 'GET') {
      if (!this.gateway) {
        this.json(res, 503, { error: 'Gateway not configured' });
        return;
      }
      const authHeader = req.headers['authorization'];
      if (!authHeader?.startsWith('Bearer ')) {
        this.json(res, 401, { error: 'Missing Bearer token' });
        return;
      }
      try {
        const token = this.gateway.verifyToken(authHeader.slice(7));
        const status = this.gateway.getStatus(token);
        this.json(res, 200, status);
      } catch (err) {
        if (err instanceof GatewayError) {
          this.json(res, err.statusCode, { error: err.message });
          return;
        }
        this.json(res, 500, { error: String(err) });
      }
      return;
    }

    // ── Gateway: Manual / Handbook ──────────────────────────────────────────
    if (path === '/api/gateway/manual' && req.method === 'GET') {
      if (!this.gateway) {
        this.json(res, 503, { error: 'Gateway not configured' });
        return;
      }
      const authHeader = req.headers['authorization'];
      if (!authHeader?.startsWith('Bearer ')) {
        this.json(res, 401, { error: 'Missing Bearer token' });
        return;
      }
      try {
        const token = this.gateway.verifyToken(authHeader.slice(7));
        const reg = this.gateway.listRegistrations(token.orgId)
          .find(r => r.externalAgentId === token.externalAgentId);

        const colleagues: HandbookColleague[] = this.orgService.getAgentManager().listAgents()
          .filter(a => a.id !== token.markusAgentId)
          .map(a => ({ id: a.id, name: a.name, role: a.role, status: a.status }));
        const mgr = colleagues.find(c => {
          const all = this.orgService.getAgentManager().listAgents();
          return all.find(a => a.id === c.id && a.agentRole === 'manager');
        });

        const projects: HandbookProject[] = this.projectService
          ? this.projectService.listProjects(token.orgId).map(p => ({
              id: p.id, name: p.name,
            }))
          : [];

        const handbook = generateHandbook({
          baseUrl: `http://localhost:${this.port}`,
          orgName: token.orgId,
          agentName: reg?.agentName,
          markusAgentId: token.markusAgentId,
          platform: reg?.platform,
          colleagues,
          manager: mgr ? { id: mgr.id, name: mgr.name } : undefined,
          projects,
        });
        res.writeHead(200, { 'Content-Type': 'text/markdown; charset=utf-8' });
        res.end(handbook);
      } catch (err) {
        if (err instanceof GatewayError) {
          this.json(res, err.statusCode, { error: err.message });
          return;
        }
        this.json(res, 500, { error: String(err) });
      }
      return;
    }

    // ── Gateway: Team Context ────────────────────────────────────────────────
    if (path === '/api/gateway/team' && req.method === 'GET') {
      if (!this.gateway) { this.json(res, 503, { error: 'Gateway not configured' }); return; }
      const authHeader = req.headers['authorization'];
      if (!authHeader?.startsWith('Bearer ')) { this.json(res, 401, { error: 'Missing Bearer token' }); return; }
      try {
        const token = this.gateway.verifyToken(authHeader.slice(7));
        const agents = this.orgService.getAgentManager().listAgents();
        const colleagues = agents
          .filter(a => a.id !== token.markusAgentId)
          .map(a => ({ id: a.id, name: a.name, role: a.role, status: a.status, agentRole: a.agentRole, skills: a.skills }));
        const manager = agents.find(a => a.agentRole === 'manager' && a.id !== token.markusAgentId);
        this.json(res, 200, {
          colleagues,
          manager: manager ? { id: manager.id, name: manager.name } : null,
        });
      } catch (err) {
        if (err instanceof GatewayError) { this.json(res, err.statusCode, { error: err.message }); return; }
        this.json(res, 500, { error: String(err) });
      }
      return;
    }

    // ── Gateway: Projects ────────────────────────────────────────────────────
    if (path === '/api/gateway/projects' && req.method === 'GET') {
      if (!this.gateway) { this.json(res, 503, { error: 'Gateway not configured' }); return; }
      const authHeader = req.headers['authorization'];
      if (!authHeader?.startsWith('Bearer ')) { this.json(res, 401, { error: 'Missing Bearer token' }); return; }
      try {
        const token = this.gateway.verifyToken(authHeader.slice(7));
        if (!this.projectService) { this.json(res, 200, { projects: [] }); return; }
        const projects = this.projectService.listProjects(token.orgId).map(p => ({
          id: p.id, name: p.name, description: p.description, status: p.status,
          teamIds: p.teamIds,
        }));
        this.json(res, 200, { projects });
      } catch (err) {
        if (err instanceof GatewayError) { this.json(res, err.statusCode, { error: err.message }); return; }
        this.json(res, 500, { error: String(err) });
      }
      return;
    }

    // ── Gateway: Requirements ────────────────────────────────────────────────
    if (path === '/api/gateway/requirements' && req.method === 'GET') {
      if (!this.gateway) { this.json(res, 503, { error: 'Gateway not configured' }); return; }
      const authHeader = req.headers['authorization'];
      if (!authHeader?.startsWith('Bearer ')) { this.json(res, 401, { error: 'Missing Bearer token' }); return; }
      try {
        const token = this.gateway.verifyToken(authHeader.slice(7));
        if (!this.requirementService) { this.json(res, 200, { requirements: [] }); return; }
        const url = new URL(req.url!, `http://localhost`);
        const projectId = url.searchParams.get('project_id') ?? undefined;
        const status = url.searchParams.get('status') ?? undefined;
        const reqs = this.requirementService.listRequirements({
          orgId: token.orgId,
          projectId,
          status: status as any,
        }).map(r => ({
          id: r.id, title: r.title, description: r.description,
          status: r.status, priority: r.priority,
          projectId: r.projectId,
          source: r.source, createdAt: r.createdAt,
        }));
        this.json(res, 200, { requirements: reqs });
      } catch (err) {
        if (err instanceof GatewayError) { this.json(res, err.statusCode, { error: err.message }); return; }
        this.json(res, 500, { error: String(err) });
      }
      return;
    }

    // ── Gateway: Deliverables ──────────────────────────────────────────────
    if (path === '/api/gateway/deliverables' && req.method === 'GET') {
      if (!this.gateway) { this.json(res, 503, { error: 'Gateway not configured' }); return; }
      const authHeader = req.headers['authorization'];
      if (!authHeader?.startsWith('Bearer ')) { this.json(res, 401, { error: 'Missing Bearer token' }); return; }
      try {
        this.gateway.verifyToken(authHeader.slice(7));
        if (!this.deliverableService) { this.json(res, 503, { error: 'Deliverable service not available' }); return; }
        const q = url.searchParams.get('q') ?? undefined;
        const projectId = url.searchParams.get('projectId') ?? undefined;
        const type = url.searchParams.get('type') as any ?? undefined;
        const { results } = this.deliverableService.search({ query: q, projectId, type });
        this.json(res, 200, { results });
      } catch (err) {
        if (err instanceof GatewayError) { this.json(res, err.statusCode, { error: err.message }); return; }
        this.json(res, 500, { error: String(err) });
      }
      return;
    }

    if (path === '/api/gateway/deliverables' && req.method === 'POST') {
      if (!this.gateway) { this.json(res, 503, { error: 'Gateway not configured' }); return; }
      const authHeader = req.headers['authorization'];
      if (!authHeader?.startsWith('Bearer ')) { this.json(res, 401, { error: 'Missing Bearer token' }); return; }
      try {
        const token = this.gateway.verifyToken(authHeader.slice(7));
        if (!this.deliverableService) { this.json(res, 503, { error: 'Deliverable service not available' }); return; }
        const body = await this.readBody(req);
        const d = await this.deliverableService.create({
          type: body['type'] as any ?? 'text',
          title: body['title'] as string,
          summary: body['summary'] as string ?? body['content'] as string,
          reference: body['reference'] as string,
          format: body['format'] as string | undefined,
          tags: body['tags'] as string[],
          agentId: token.markusAgentId,
          projectId: body['projectId'] as string,
        });
        this.json(res, 201, { deliverable: d });
      } catch (err) {
        if (err instanceof GatewayError) { this.json(res, err.statusCode, { error: err.message }); return; }
        this.json(res, 500, { error: String(err) });
      }
      return;
    }

    if (path.match(/^\/api\/gateway\/deliverables\/[^/]+$/) && req.method === 'PUT') {
      if (!this.gateway) { this.json(res, 503, { error: 'Gateway not configured' }); return; }
      const authHeader = req.headers['authorization'];
      if (!authHeader?.startsWith('Bearer ')) { this.json(res, 401, { error: 'Missing Bearer token' }); return; }
      try {
        this.gateway.verifyToken(authHeader.slice(7));
        if (!this.deliverableService) { this.json(res, 503, { error: 'Deliverable service not available' }); return; }
        const delivId = path.split('/')[4]!;
        const body = await this.readBody(req);
        const d = await this.deliverableService.update(delivId, {
          title: body['title'] as string | undefined,
          summary: body['summary'] as string | undefined,
          reference: body['reference'] as string | undefined,
          status: body['status'] as any,
          type: body['type'] as any,
          tags: body['tags'] as string[] | undefined,
        });
        if (!d) { this.json(res, 404, { error: 'Deliverable not found' }); return; }
        this.json(res, 200, { deliverable: d });
      } catch (err) {
        if (err instanceof GatewayError) { this.json(res, err.statusCode, { error: err.message }); return; }
        this.json(res, 500, { error: String(err) });
      }
      return;
    }

    // ── Gateway: Sync Endpoint ──────────────────────────────────────────────
    if (path === '/api/gateway/sync' && req.method === 'POST') {
      if (!this.gateway || !this.syncHandler) {
        this.json(res, 503, { error: 'Gateway not configured' });
        return;
      }
      const authHeader = req.headers['authorization'];
      if (!authHeader?.startsWith('Bearer ')) {
        this.json(res, 401, { error: 'Missing Bearer token' });
        return;
      }
      try {
        const token = this.gateway.verifyToken(authHeader.slice(7));
        const body = await this.readBody(req) as SyncRequest;
        const result = await this.syncHandler.handleSync(token.markusAgentId, token.orgId, body);
        this.json(res, 200, result);
      } catch (err) {
        if (err instanceof GatewayError) {
          this.json(res, err.statusCode, { error: err.message });
          return;
        }
        this.json(res, 500, { error: String(err) });
      }
      return;
    }

    // ── Gateway: Task Lifecycle Endpoints ────────────────────────────────────
    const gwTaskMatch = path.match(/^\/api\/gateway\/tasks\/([^/]+)\/(accept|progress|complete|fail|delegate|subtasks)$/);
    if (gwTaskMatch && req.method === 'POST') {
      if (!this.gateway) {
        this.json(res, 503, { error: 'Gateway not configured' });
        return;
      }
      const authHeader = req.headers['authorization'];
      if (!authHeader?.startsWith('Bearer ')) {
        this.json(res, 401, { error: 'Missing Bearer token' });
        return;
      }
      try {
        const token = this.gateway.verifyToken(authHeader.slice(7));
        const taskId = gwTaskMatch[1]!;
        const action = gwTaskMatch[2]!;
        const body = await this.readBody(req);

        switch (action) {
          case 'accept': {
            const task = this.taskService.updateTaskStatus(taskId, 'in_progress', `ext:${token.markusAgentId}`);
            this.json(res, 200, { task: { id: task.id, status: task.status } });
            break;
          }
          case 'progress': {
            const task = this.taskService.getTask(taskId);
            if (!task) { this.json(res, 404, { error: 'Task not found' }); break; }
            if (task.status !== 'in_progress') {
              try { this.taskService.updateTaskStatus(taskId, 'in_progress', `ext:${token.markusAgentId}`); } catch { /* already in_progress */ }
            }
            this.json(res, 200, { taskId, progress: body['progress'], acknowledged: true });
            break;
          }
          case 'complete': {
            const task = this.taskService.updateTaskStatus(taskId, 'completed', `ext:${token.markusAgentId}`);
            this.json(res, 200, { task: { id: task.id, status: task.status } });
            break;
          }
          case 'fail': {
            const task = this.taskService.updateTaskStatus(taskId, 'failed', `ext:${token.markusAgentId}`);
            this.json(res, 200, { task: { id: task.id, status: task.status } });
            break;
          }
          case 'delegate': {
            this.json(res, 400, { error: 'Delegation is not supported — tasks must always have an assigned agent' });
            break;
          }
          case 'subtasks': {
            const parentTask = this.taskService.getTask(taskId);
            if (!parentTask) { this.json(res, 404, { error: 'Parent task not found' }); break; }
            const subtask = this.taskService.addSubtask(taskId, body['title'] as string);
            this.json(res, 201, { task: { id: subtask.id, title: subtask.title, status: subtask.status } });
            break;
          }
        }
      } catch (err) {
        if (err instanceof GatewayError) {
          this.json(res, err.statusCode, { error: err.message });
          return;
        }
        this.json(res, 500, { error: String(err) });
      }
      return;
    }

    // Human Users
    if (path === '/api/users' && req.method === 'GET') {
      const targetOrgId = url.searchParams.get('orgId') ?? 'default';
      let users = this.orgService.listHumanUsers(targetOrgId) as unknown as Array<Record<string, unknown>>;
      if (this.storage) {
        const dbUsers = await this.storage.userRepo.listByOrg(targetOrgId);
        const dbMap = new Map(dbUsers.map((u: any) => [u.id, u]));
        users = users.map(u => {
          const dbU = dbMap.get(u.id as string) as Record<string, unknown> | undefined;
          return {
            ...u,
            ...(dbU?.avatarUrl ? { avatarUrl: dbU.avatarUrl } : {}),
            hasJoined: !!dbU?.passwordHash,
          };
        });
      }
      this.json(res, 200, { users });
      return;
    }

    if (path === '/api/users' && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const body = await this.readBody(req);
      const orgId = (body['orgId'] as string) ?? 'default';
      const name = body['name'] as string;
      const role = (body['role'] as 'owner' | 'admin' | 'member' | 'guest') ?? 'member';
      const email = body['email'] as string | undefined;
      const password = body['password'] as string | undefined;
      const teamId = body['teamId'] as string | undefined;
      const userId = (body['id'] as string | undefined) ?? genUserId();

      // Always persist to DB for durability (not just when email/password provided)
      let inviteToken: string | undefined;
      let effectiveUserId = userId;
      if (this.storage) {
        if (password && !email) {
          this.json(res, 400, { error: 'Email is required when setting a password' });
          return;
        }

        // Check if a soft-deleted user with this email exists — reactivate instead of creating new
        const deletedUser = email ? this.storage.userRepo.findDeletedByEmail(email) : null;
        if (deletedUser) {
          effectiveUserId = deletedUser.id as string;
          this.storage.userRepo.reactivate(effectiveUserId, { name, role });
          if (password) {
            const hash = await hashPassword(password);
            await this.storage.userRepo.updatePassword(effectiveUserId, hash);
          } else {
            inviteToken = generateInviteToken();
            const expires = new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString();
            this.storage.userRepo.setInviteToken(effectiveUserId, inviteToken, expires);
          }
        } else {
          const passwordHash = password ? await hashPassword(password) : undefined;
          await this.storage.userRepo.create({
            id: effectiveUserId,
            orgId,
            name,
            email: email ?? undefined,
            role,
            passwordHash,
          });
          // Generate invite token for email-only users (no password set)
          if (email && !password) {
            inviteToken = generateInviteToken();
            const expires = new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString();
            this.storage.userRepo.setInviteToken(effectiveUserId, inviteToken, expires);
          }
        }
      }

      const user = this.orgService.addHumanUser(orgId, name, role, { id: effectiveUserId, email });

      // Add to team if specified
      let teamError: string | undefined;
      if (teamId) {
        try {
          this.orgService.addMemberToTeam(teamId, effectiveUserId, 'human');
        } catch (err) {
          teamError = String(err);
        }
      }

      this.auditService?.record({
        orgId,
        type: 'user_created',
        action: 'create_user',
        detail: `User "${name}" created`,
        userId: authUser.userId,
        success: true,
        metadata: { newUserId: effectiveUserId, role },
      });
      this.json(res, 201, { user, inviteToken, ...(teamError ? { teamError } : {}) });
      return;
    }

    if (path.match(/^\/api\/users\/[^/]+$/) && req.method === 'PATCH') {
      const authUser = await this.getAuthUser(req);
      const userId = path.split('/')[3]!;
      const body = await this.readBody(req);
      const updates: { name?: string; role?: string; email?: string } = {};
      if (body['name'] !== undefined) updates.name = body['name'] as string;
      if (body['role'] !== undefined) updates.role = body['role'] as string;
      if (body['email'] !== undefined) updates.email = body['email'] as string;
      try {
        const user = this.orgService.updateHumanUser(userId, updates as any);
        this.auditService?.record({
          orgId: user.orgId,
          type: 'user_updated',
          action: 'update_user',
          detail: `User "${user.name}" updated`,
          userId: authUser?.userId,
          success: true,
          metadata: { targetUserId: userId, updates },
        });
        this.json(res, 200, { user });
      } catch (err) {
        this.json(res, 400, { error: err instanceof Error ? err.message : String(err) });
      }
      return;
    }

    if (path.match(/^\/api\/users\/[^/]+\/reset-password$/) && req.method === 'POST') {
      const authUser = await this.getAuthUser(req);
      const userId = path.split('/')[3]!;
      const body = await this.readBody(req);
      const newPassword = body['password'] as string;
      if (!newPassword || newPassword.length < 6) {
        this.json(res, 400, { error: 'Password must be at least 6 characters' });
        return;
      }
      if (!this.storage) {
        this.json(res, 500, { error: 'Storage not available' });
        return;
      }
      try {
        const hash = await hashPassword(newPassword);
        await this.storage.userRepo.updatePassword(userId, hash);
        this.auditService?.record({
          orgId: 'default',
          type: 'user_updated',
          action: 'reset_password',
          detail: `Password reset for user ${userId}`,
          userId: authUser?.userId,
          success: true,
          metadata: { targetUserId: userId },
        });
        this.json(res, 200, { ok: true });
      } catch (err) {
        this.json(res, 400, { error: err instanceof Error ? err.message : String(err) });
      }
      return;
    }

    if (path.match(/^\/api\/users\/[^/]+\/reinvite$/) && req.method === 'POST') {
      if (!this.storage) {
        this.json(res, 503, { error: 'Storage not available' });
        return;
      }
      const userId = path.split('/')[3]!;
      const userRow = this.storage.userRepo.findById(userId);
      if (!userRow) {
        this.json(res, 404, { error: 'User not found' });
        return;
      }
      if (!userRow.email) {
        this.json(res, 400, { error: 'User has no email address' });
        return;
      }
      const inviteToken = generateInviteToken();
      const expires = new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString();
      this.storage.userRepo.setInviteToken(userId, inviteToken, expires);
      this.json(res, 200, { inviteToken });
      return;
    }

    if (path.startsWith('/api/users/') && req.method === 'DELETE') {
      const authUser = await this.getAuthUser(req);
      const userId = path.split('/')[3]!;
      this.orgService.removeHumanUser(userId);
      this.auditService?.record({
        orgId: 'default',
        type: 'user_deleted',
        action: 'delete_user',
        detail: `User ${userId} removed`,
        userId: authUser?.userId,
        success: true,
        metadata: { deletedUserId: userId },
      });
      this.json(res, 200, { deleted: true });
      return;
    }

    // Message routing — route to the right agent
    if (path === '/api/message' && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const body = await this.readBody(req);
      const targetOrgId = (body['orgId'] as string) ?? 'default';
      const targetAgentId = this.orgService.routeMessage(targetOrgId, {
        targetAgentId: body['targetAgentId'] as string | undefined,
        channelId: body['channelId'] as string | undefined,
        text: body['text'] as string | undefined,
      });

      if (!targetAgentId) {
        this.json(res, 404, { error: 'No agent available to handle the message' });
        return;
      }

      const senderId = authUser.userId;
      const images = (body['images'] as string[] | undefined)?.filter(Boolean);
      const senderInfo = this.orgService.resolveHumanIdentity(senderId);
      const agent = this.orgService.getAgentManager().getAgent(targetAgentId);
      this.ws.broadcastAgentUpdate(targetAgentId, 'working');

      const stream = body['stream'] as boolean | undefined;
      if (stream) {
        const userText = body['text'] as string;

        const sseHandler = new SSEHandler({
          agentId: targetAgentId,
          agent,
          userText,
          images,
          senderId,
          senderInfo,
          executionStreamRepo: this.storage?.executionStreamRepo,
          onComplete: async (reply, segments, tokensUsed) => {
            const meta = segments.length > 0 ? { segments } : undefined;
            void this.persistChatTurn(targetAgentId, userText, reply, senderId, tokensUsed, meta);
          },
        });

        await sseHandler.handle(res);
      } else {
        const userText = body['text'] as string;
        const toolEvents: Array<{ tool: string; status: 'done' | 'error'; arguments?: unknown; result?: string; durationMs?: number }> = [];
        let reply: string;
        try {
          reply = await agent.sendMessage(userText, senderId, senderInfo, { images, toolEventCollector: toolEvents });
        } catch (err) {
          throw err;
        }
        this.json(res, 200, { reply, agentId: targetAgentId });
        const { thinking, clean: cleanReply } = extractThinkBlocks(reply);
        const segments: Array<Record<string, unknown>> = [];
        if (thinking.length > 0) segments.push({ type: 'text', content: '', thinking: thinking.join('\n\n') });
        for (const te of toolEvents) {
          segments.push({ type: 'tool', tool: te.tool, status: te.status, arguments: te.arguments, result: te.result, durationMs: te.durationMs });
        }
        if (segments.length > 0) segments.push({ type: 'text', content: cleanReply });
        const meta = segments.length > 0 ? { segments } : undefined;
        void this.persistChatTurn(targetAgentId, userText, reply, senderId, agent.getState().tokensUsedToday, meta);
      }
      const _st2 = agent.getState();
      this.ws.broadcastAgentUpdate(targetAgentId, _st2.status, { lastError: _st2.lastError, lastErrorAt: _st2.lastErrorAt, currentActivity: _st2.currentActivity });
      return;
    }

    // Skills
    if (path === '/api/skills' && req.method === 'GET') {
      // Skills from in-memory registry
      const registrySkills = (this.skillRegistry?.list() ?? [])
        .map(s => ({
          name: s.name,
          version: s.version,
          description: s.description,
          author: s.author,
          category: s.category,
          tags: s.tags,
          hasInstructions: !!s.instructions,
          sourcePath: s.builtIn ? undefined : s.sourcePath,
          type: (s.builtIn ? 'builtin' : s.sourcePath ? 'filesystem' : 'registry') as string,
        }));
      const seen = new Set(registrySkills.map(s => s.name));

      // Live filesystem scan for any skills not yet in registry
      const fsSkills: Array<{ name: string; version: string; description?: string; author?: string; category?: string; tags?: string[]; hasInstructions: boolean; sourcePath: string; type: string }> = [];
      for (const dir of WELL_KNOWN_SKILL_DIRS) {
        for (const discovered of discoverSkillsInDir(dir)) {
          if (seen.has(discovered.manifest.name)) continue;
          seen.add(discovered.manifest.name);
          fsSkills.push({
            name: discovered.manifest.name,
            version: discovered.manifest.version,
            description: discovered.manifest.description,
            author: discovered.manifest.author,
            category: discovered.manifest.category,
            tags: discovered.manifest.tags,
            hasInstructions: !!discovered.manifest.instructions,
            sourcePath: discovered.path,
            type: 'filesystem',
          });
        }
      }

      const imported: Array<{ name: string; description: string; category: string; version: string; tags: string[]; hasInstructions: boolean; type: string }> = [];

      const agents = this.orgService.getAgentManager().listAgents();
      const skillAgents: Record<string, string[]> = {};
      for (const agent of agents) {
        for (const skillName of agent.skills) {
          if (!skillAgents[skillName]) skillAgents[skillName] = [];
          skillAgents[skillName]!.push(agent.id);
        }
      }
      const all = [
        ...registrySkills.map(s => ({ ...s, agentIds: skillAgents[s.name] ?? [] })),
        ...fsSkills.map(s => ({ ...s, agentIds: skillAgents[s.name] ?? [] })),
        ...imported.map(s => ({ ...s, agentIds: skillAgents[s.name] ?? [] })),
      ];
      this.json(res, 200, { skills: all });
      return;
    }

    // Built-in skills — list templates/skills/
    if (path === '/api/skills/builtin' && req.method === 'GET') {
      const builtinDir = resolve(process.cwd(), 'templates', 'skills');
      const found = discoverSkillsInDir(builtinDir);
      const installedSkills = new Map(
        (this.skillRegistry?.list() ?? []).map(s => [s.name, s])
      );
      // Read raw manifests to get i18n/hidden fields
      const rawManifests = new Map<string, Record<string, unknown>>();
      try {
        const { readdirSync, readFileSync, existsSync } = await import('node:fs');
        for (const entry of readdirSync(builtinDir, { withFileTypes: true })) {
          if (!entry.isDirectory()) continue;
          const sjPath = resolve(builtinDir, entry.name, 'skill.json');
          if (existsSync(sjPath)) {
            try { rawManifests.set(entry.name, JSON.parse(readFileSync(sjPath, 'utf-8'))); } catch { /* skip */ }
          }
        }
      } catch { /* skip */ }
      const skills = found
        .filter(({ manifest }) => {
          const raw = rawManifests.get(manifest.name);
          return !(raw as Record<string, unknown>)?.hidden;
        })
        .map(({ manifest, path: p }) => {
          const inst = installedSkills.get(manifest.name);
          const raw = rawManifests.get(manifest.name);
          return {
            name: manifest.name,
            version: manifest.version,
            description: manifest.description,
            author: manifest.author,
            category: manifest.category,
            tags: manifest.tags ?? [],
            hasMcpServers: !!manifest.mcpServers && Object.keys(manifest.mcpServers).length > 0,
            hasInstructions: !!manifest.instructions,
            instructions: manifest.instructions ?? undefined,
            requiredPermissions: manifest.requiredPermissions ?? [],
            sourcePath: p,
            installed: !!inst,
            installedVersion: inst?.version ?? null,
            i18n: (raw as Record<string, unknown>)?.i18n ?? undefined,
          };
        });
      this.json(res, 200, { skills });
      return;
    }

    if (path.match(/^\/api\/skills\/[^/]+$/) && req.method === 'GET') {
      const skillName = decodeURIComponent(path.split('/')[3]!);
      if (!this.skillRegistry) {
        this.json(res, 404, { error: 'Skill registry not configured' });
        return;
      }
      const skill = this.skillRegistry.get(skillName);
      if (!skill) {
        this.json(res, 404, { error: `Skill not found: ${skillName}` });
        return;
      }
      const manifest = skill.manifest;
      this.json(res, 200, {
        skill: {
          ...manifest,
          hasInstructions: !!manifest.instructions,
          instructionsPreview: manifest.instructions?.slice(0, 500),
        },
      });
      return;
    }

    // Third-party skill registry — fetch from GitHub repos and cache
    if (path === '/api/skills/registry' && req.method === 'GET') {
      const source = url.searchParams.get('source') ?? 'openclaw';
      const now = Date.now();
      const cacheKey = `skill-registry-${source}`;
      const cached = this.registryCache?.get(cacheKey);
      if (cached && now - cached.ts < 600_000) {
        this.json(res, 200, { skills: cached.data, source, cached: true });
        return;
      }

      try {
        const skills: Array<{ name: string; description: string; category: string; source: string; sourceUrl: string; author: string; addedAt?: string }> = [];

        if (source === 'openclaw') {
          const resp = await fetch('https://raw.githubusercontent.com/LeoYeAI/openclaw-master-skills/main/README.md');
          if (resp.ok) {
            const readme = await resp.text();
            const tableLines = readme.split('\n').filter(l => l.startsWith('| ['));
            for (const line of tableLines) {
              const cols = line.split('|').map(c => c.trim()).filter(Boolean);
              if (cols.length >= 4) {
                const nameMatch = cols[0]?.match(/\[([^\]]+)\]/);
                const name = nameMatch?.[1] ?? '';
                const description = cols[1]?.replace(/\.\.\.$/, '').trim() ?? '';
                const category = cols[2]?.trim() ?? 'Other';
                const srcMatch = cols[3]?.match(/\[GitHub\]\(([^)]+)\)/);
                const addedAt = cols[4]?.trim();
                if (name) {
                  skills.push({
                    name,
                    description,
                    category,
                    source: 'openclaw',
                    sourceUrl: srcMatch?.[1] ?? `https://github.com/LeoYeAI/openclaw-master-skills/tree/main/skills/${name}`,
                    author: 'Community',
                    addedAt,
                  });
                }
              }
            }
          }
        }

        if (!this.registryCache) this.registryCache = new Map();
        this.registryCache.set(cacheKey, { data: skills, ts: now });
        this.json(res, 200, { skills, source, cached: false });
      } catch (err) {
        this.json(res, 500, { error: `Failed to fetch registry: ${String(err)}` });
      }
      return;
    }

    // Install a skill: download to ~/.markus/skills/ and register
    if (path === '/api/skills/install' && req.method === 'POST') {
      const body = await this.readBody(req);
      const skillName = body['name'] as string;
      if (!skillName) {
        this.json(res, 400, { error: 'name is required' });
        return;
      }

      try {
        const result = await installSkill({
          name: skillName,
          source: body['source'] as string | undefined,
          slug: body['slug'] as string | undefined,
          sourceUrl: body['sourceUrl'] as string | undefined,
          description: body['description'] as string | undefined,
          category: body['category'] as string | undefined,
          version: body['version'] as string | undefined,
          githubRepo: body['githubRepo'] as string | undefined,
          githubSkillPath: body['githubSkillPath'] as string | undefined,
        }, this.skillRegistry);

        this.json(res, 201, result);
        return;
      } catch (err) {
        const msg = String(err instanceof Error ? err.message : err);
        const status = msg.includes('Download failed') ? 502 : 500;
        this.json(res, status, { error: msg });
        return;
      }
    }

    // Uninstall a skill: delete from filesystem and/or DB
    if (path.startsWith('/api/skills/installed/') && req.method === 'DELETE') {
      const skillName = decodeURIComponent(path.slice('/api/skills/installed/'.length));
      if (!skillName) {
        this.json(res, 400, { error: 'skill name is required' });
        return;
      }

      let deletedFs = false;

      // Try delete from filesystem (~/.markus/skills/)
      const skillsDir = join(homedir(), '.markus', 'skills');
      const safeName = skillName.replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase();
      const targetDir = join(skillsDir, safeName);
      if (existsSync(targetDir)) {
        try {
          execSync(`rm -rf "${targetDir}"`, { timeout: 10000 });
          deletedFs = true;
        } catch (err) {
          console.warn(`[skills/uninstall] fs delete failed: ${String(err)}`);
        }
      }

      if (!deletedFs) {
        this.json(res, 404, { error: `Skill "${skillName}" not found` });
        return;
      }

      // Unregister from runtime SkillRegistry
      if (this.skillRegistry) {
        this.skillRegistry.unregister(skillName);
      }

      // Remove from all agents that had this skill assigned
      const agentMgr = this.orgService.getAgentManager();
      const affectedAgents: string[] = [];
      for (const agentInfo of agentMgr.listAgents()) {
        try {
          const agent = agentMgr.getAgent(agentInfo.id);
          if (agent.config.skills.includes(skillName)) {
            agent.config.skills = agent.config.skills.filter(s => s !== skillName);
            affectedAgents.push(agentInfo.id);
            if (this.storage) {
              try { await this.storage.agentRepo.updateConfig(agentInfo.id, { skills: agent.config.skills }); }
              catch (e) { log.warn('Failed to persist skill removal from agent after uninstall', { agentId: agentInfo.id, error: String(e) }); }
            }
          }
        } catch { /* agent not accessible */ }
      }

      this.json(res, 200, { deleted: true, name: skillName, deletedFs, removedFromAgents: affectedAgents });
      return;
    }

    // ── Builder Artifacts: directory-based package management ──────────────

    // GET /api/builder/artifacts — scan all builder artifacts
    if (path === '/api/builder/artifacts' && req.method === 'GET') {
      try {
        const artifacts = this.builderService
          ? this.builderService.listArtifacts()
          : [];
        this.json(res, 200, { artifacts });
      } catch (err) {
        this.json(res, 500, { error: `Scan failed: ${String(err)}` });
      }
      return;
    }

    // GET /api/builder/artifacts/:type/:name — read one artifact (all files)
    {
      const artMatch = path.match(/^\/api\/builder\/artifacts\/(agents?|teams?|skills?)\/([^/]+)$/);
      if (artMatch && req.method === 'GET') {
        const rawType = artMatch[1]!;
        const name = decodeURIComponent(artMatch[2]!);
        const typeDir = rawType.endsWith('s') ? rawType : rawType + 's';
        const artDir = join(homedir(), '.markus', 'builder-artifacts', typeDir, name);
        if (!existsSync(artDir)) {
          this.json(res, 404, { error: 'Artifact not found' });
          return;
        }
        try {
          const files: Record<string, string> = {};
          const readDir = (dir: string, prefix: string): void => {
            for (const entry of readdirSync(dir, { withFileTypes: true })) {
              const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
              if (entry.isDirectory()) {
                readDir(join(dir, entry.name), relPath);
              } else {
                try { files[relPath] = readFileSync(join(dir, entry.name), 'utf-8'); } catch { /* skip binary */ }
              }
            }
          };
          readDir(artDir, '');
          const type = typeDir === 'agents' ? 'agent' : typeDir === 'teams' ? 'team' : 'skill';
          this.json(res, 200, { type, name, path: artDir, files });
        } catch (err) {
          this.json(res, 500, { error: `Read failed: ${String(err)}` });
        }
        return;
      }
    }

    // GET /api/builder/artifacts/installed — detect which artifacts have been installed
    if (path === '/api/builder/artifacts/installed' && req.method === 'GET') {
      try {
        const installed: Record<string, { agentId?: string; agentIds?: string[]; teamId?: string }> = {};
        const agentManager = this.orgService.getAgentManager();
        const dataDir = agentManager.getDataDir();

        // Scan agents for .role-origin.json markers
        for (const agentInfo of agentManager.listAgents()) {
          const originPath = join(dataDir, agentInfo.id, 'role', '.role-origin.json');
          if (existsSync(originPath)) {
            try {
              const origin = JSON.parse(readFileSync(originPath, 'utf-8'));
              if (origin.source === 'builder-artifact' && origin.artifact) {
                const artName = origin.artifact as string;
                let artType = origin.artifactType as string | undefined;
                if (!artType) {
                  try {
                    const agentObj = agentManager.getAgent(agentInfo.id);
                    artType = agentObj.config.teamId ? 'team' : 'agent';
                  } catch { artType = 'agent'; }
                }
                if (artType === 'team') {
                  const teamKey = `team/${artName}`;
                  if (!installed[teamKey]) installed[teamKey] = { agentIds: [] };
                  installed[teamKey].agentIds!.push(agentInfo.id);
                  if (!installed[teamKey].teamId) {
                    try {
                      const agentObj = agentManager.getAgent(agentInfo.id);
                      if (agentObj.config.teamId) installed[teamKey].teamId = agentObj.config.teamId;
                    } catch { /* skip */ }
                  }
                } else {
                  installed[`agent/${artName}`] = { agentId: agentInfo.id };
                }
              }
            } catch { /* skip invalid */ }
          }
        }

        // Scan skills: check builder-artifacts paired with installed skills
        const skillArtDir = join(homedir(), '.markus', 'builder-artifacts', 'skills');
        const skillsDir = join(homedir(), '.markus', 'skills');
        if (existsSync(skillArtDir)) {
          try {
            for (const entry of readdirSync(skillArtDir, { withFileTypes: true })) {
              if (entry.isDirectory() && existsSync(join(skillsDir, entry.name))) {
                installed[`skill/${entry.name}`] = {};
              }
            }
          } catch { /* ignore */ }
        }
        // Also detect skills installed directly (skillhub/skillssh/builtin) without builder-artifacts
        if (existsSync(skillsDir)) {
          try {
            for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
              const key = `skill/${entry.name}`;
              if (entry.isDirectory() && !installed[key]) {
                installed[key] = {};
              }
            }
          } catch { /* ignore */ }
        }

        this.json(res, 200, { installed });
      } catch (err) {
        this.json(res, 500, { error: `Scan failed: ${String(err)}` });
      }
      return;
    }

    // POST /api/builder/artifacts/save — save JSON artifact as directory-based package
    if (path === '/api/builder/artifacts/save' && req.method === 'POST') {
      const body = await this.readBody(req);
      const mode = body['mode'] as string;
      const artifact = body['artifact'] as Record<string, unknown>;
      if (!mode || !['agent', 'team', 'skill'].includes(mode) || !artifact) {
        this.json(res, 400, { error: 'mode must be agent|team|skill and artifact is required' });
        return;
      }

      try {
        const typeDir = mode === 'agent' ? 'agents' : mode === 'team' ? 'teams' : 'skills';
        const pkgType = mode as PackageType;
        const manifest = buildManifest(pkgType, artifact);
        if (!manifest.source) manifest.source = { type: 'local' };
        const mfName = manifestFilename(pkgType);
        const artDir = join(homedir(), '.markus', 'builder-artifacts', typeDir, manifest.name);
        mkdirSync(artDir, { recursive: true });
        writeFileSync(join(artDir, mfName), JSON.stringify(manifest, null, 2), 'utf-8');

        // Write content files from `files` map
        const artFiles = artifact.files as Record<string, string> | undefined;
        if (artFiles) {
          for (const [fn, c] of Object.entries(artFiles)) {
            if (fn === mfName) continue;
            const filePath = join(artDir, fn);
            mkdirSync(dirname(filePath), { recursive: true });
            writeFileSync(filePath, c, 'utf-8');
          }
        }

        // Team: extract announcement, norms, and member role files from config
        if (mode === 'team') {
          const fileSet = new Set(artFiles ? Object.keys(artFiles) : []);

          // Write ANNOUNCEMENT.md / NORMS.md from config fields if not already in files
          const announcement = artifact.announcement as string | undefined;
          if (announcement && !fileSet.has('ANNOUNCEMENT.md')) {
            writeFileSync(join(artDir, 'ANNOUNCEMENT.md'), announcement, 'utf-8');
          }
          const norms = artifact.norms as string | undefined;
          if (norms && !fileSet.has('NORMS.md')) {
            writeFileSync(join(artDir, 'NORMS.md'), norms, 'utf-8');
          }

          // Write member role files from config if not already written via files map
          const rawMembers = (Array.isArray((artifact.team as Record<string, unknown>)?.members)
            ? (artifact.team as Record<string, unknown>).members
            : Array.isArray(artifact.members) ? artifact.members : []) as Array<Record<string, unknown>>;
          for (const [idx, m] of rawMembers.entries()) {
            const mName = (m.name as string) ?? 'Agent';
            const slug = kebab(mName, 'member-' + idx);
            const memberDir = join(artDir, 'members', slug);
            const roleContent = (m.roleContent as string) || (m.role_md as string);
            const policiesContent = (m.policiesContent as string) || (m.policies_md as string);
            const contextContent = (m.contextContent as string) || (m.context_md as string);
            if (roleContent && !fileSet.has(`members/${slug}/ROLE.md`)) {
              mkdirSync(memberDir, { recursive: true });
              writeFileSync(join(memberDir, 'ROLE.md'), roleContent, 'utf-8');
            }
            if (policiesContent && !fileSet.has(`members/${slug}/POLICIES.md`)) {
              mkdirSync(memberDir, { recursive: true });
              writeFileSync(join(memberDir, 'POLICIES.md'), policiesContent, 'utf-8');
            }
            if (contextContent && !fileSet.has(`members/${slug}/CONTEXT.md`)) {
              mkdirSync(memberDir, { recursive: true });
              writeFileSync(join(memberDir, 'CONTEXT.md'), contextContent, 'utf-8');
            }
          }

          // Legacy: write explicit memberFiles if provided in JSON
          const memberFiles = artifact.memberFiles as Record<string, Record<string, string>> | undefined;
          if (memberFiles) {
            for (const [slug, files] of Object.entries(memberFiles)) {
              const memberDir = join(artDir, 'members', slug);
              mkdirSync(memberDir, { recursive: true });
              for (const [fn, c] of Object.entries(files)) writeFileSync(join(memberDir, fn), c, 'utf-8');
            }
          }
        }

        if (this.deliverableService) {
          this.deliverableService.create({
            type: 'file',
            title: `${mode.charAt(0).toUpperCase() + mode.slice(1)}: ${manifest.displayName}`,
            summary: (artifact.description as string) ?? manifest.description ?? `${mode} saved via Builder`,
            reference: artDir,
            artifactType: mode as 'agent' | 'team' | 'skill',
            artifactData: artifact,
            tags: ['builder', mode],
          }).catch(err => log.warn('Failed to create deliverable for builder artifact', { error: String(err) }));
        }

        this.json(res, 201, { type: mode, name: manifest.name, path: artDir });
      } catch (err) {
        this.json(res, 500, { error: `Save failed: ${String(err)}` });
      }
      return;
    }

    // POST /api/builder/artifacts/import — write a bundle of files directly to artifact directory
    if (path === '/api/builder/artifacts/import' && req.method === 'POST') {
      const body = await this.readBody(req);
      const type = body['type'] as string;
      const name = body['name'] as string;
      const files = body['files'] as Record<string, string> | undefined;
      const source = body['source'] as { type: string; hubItemId?: string; url?: string } | undefined;
      if (!type || !['agent', 'team', 'skill'].includes(type) || !name || !files) {
        this.json(res, 400, { error: 'type (agent|team|skill), name, and files are required' });
        return;
      }
      try {
        const typeDir = type === 'agent' ? 'agents' : type === 'team' ? 'teams' : 'skills';
        const artDir = join(homedir(), '.markus', 'builder-artifacts', typeDir, name);
        mkdirSync(artDir, { recursive: true });
        for (const [fn, content] of Object.entries(files)) {
          const filePath = join(artDir, fn);
          mkdirSync(dirname(filePath), { recursive: true });
          writeFileSync(filePath, content, 'utf-8');
        }

        // Write source tracking into manifest if source provided
        if (source) {
          const mfName = manifestFilename(type as PackageType);
          const mfPath = join(artDir, mfName);
          if (existsSync(mfPath)) {
            try {
              const mf = JSON.parse(readFileSync(mfPath, 'utf-8'));
              mf.source = source;
              writeFileSync(mfPath, JSON.stringify(mf, null, 2), 'utf-8');
            } catch { /* skip if manifest invalid */ }
          }
        }

        this.json(res, 201, { type, name, path: artDir });
      } catch (err) {
        this.json(res, 500, { error: `Import failed: ${String(err)}` });
      }
      return;
    }

    // POST /api/builder/artifacts/:type/:name/install — deploy from package to runtime
    {
      const installMatch = path.match(/^\/api\/builder\/artifacts\/(agents?|teams?|skills?)\/([^/]+)\/install$/);
      if (installMatch && req.method === 'POST') {
        const rawType = installMatch[1]!;
        const name = decodeURIComponent(installMatch[2]!);
        const typeDir = rawType.endsWith('s') ? rawType : rawType + 's';
        const type = (typeDir === 'agents' ? 'agent' : typeDir === 'teams' ? 'team' : 'skill') as 'agent' | 'team' | 'skill';

        if (!this.builderService) {
          this.json(res, 500, { error: 'BuilderService not initialized' });
          return;
        }

        try {
          const result = await this.builderService.installArtifact(type, name);
          this.json(res, 201, result);
        } catch (err) {
          const msg = String(err instanceof Error ? err.message : err);
          const status = msg.includes('not found') ? 404 : msg.includes('Invalid manifest') || msg.includes('No ') ? 400 : 500;
          this.json(res, status, { error: msg });
        }
        return;
      }
    }

    // POST /api/builder/artifacts/:type/:name/uninstall — remove deployed artifact from runtime
    {
      const uninstallMatch = path.match(/^\/api\/builder\/artifacts\/(agents?|teams?|skills?)\/([^/]+)\/uninstall$/);
      if (uninstallMatch && req.method === 'POST') {
        const rawType = uninstallMatch[1]!;
        const name = decodeURIComponent(uninstallMatch[2]!);
        const typeDir = rawType.endsWith('s') ? rawType : rawType + 's';
        const type = typeDir === 'agents' ? 'agent' : typeDir === 'teams' ? 'team' : 'skill';

        try {
          const agentManager = this.orgService.getAgentManager();
          const dataDir = agentManager.getDataDir();
          const removedAgents: string[] = [];
          let removedTeamId: string | undefined;

          if (type === 'agent') {
            for (const agentInfo of agentManager.listAgents()) {
              const originPath = join(dataDir, agentInfo.id, 'role', '.role-origin.json');
              if (existsSync(originPath)) {
                try {
                  const origin = JSON.parse(readFileSync(originPath, 'utf-8'));
                  if (origin.artifact === name && (!origin.artifactType || origin.artifactType === 'agent')) {
                    await this.orgService.fireAgent(agentInfo.id);
                    removedAgents.push(agentInfo.id);
                  }
                } catch { /* skip */ }
              }
            }
          } else if (type === 'team') {
            const teamAgentIds: string[] = [];
            let teamId: string | undefined;
            for (const agentInfo of agentManager.listAgents()) {
              const originPath = join(dataDir, agentInfo.id, 'role', '.role-origin.json');
              if (existsSync(originPath)) {
                try {
                  const origin = JSON.parse(readFileSync(originPath, 'utf-8'));
                  if (origin.artifact === name && origin.artifactType === 'team') {
                    teamAgentIds.push(agentInfo.id);
                    if (!teamId) {
                      try {
                        const agentObj = agentManager.getAgent(agentInfo.id);
                        teamId = agentObj.config.teamId;
                      } catch { /* skip */ }
                    }
                  }
                } catch { /* skip */ }
              }
            }
            // Fallback: find team by matching member agent IDs
            if (!teamId && teamAgentIds.length > 0) {
              const teams = this.orgService.listTeams('default');
              for (const t of teams) {
                if (teamAgentIds.some(aid => t.memberAgentIds.includes(aid))) {
                  teamId = t.id;
                  break;
                }
              }
            }
            if (teamId) {
              await this.orgService.deleteTeam(teamId, true);
              removedTeamId = teamId;
            } else {
              for (const aid of teamAgentIds) {
                await this.orgService.fireAgent(aid);
              }
            }
            removedAgents.push(...teamAgentIds);
          } else if (type === 'skill') {
            const skillDir = join(homedir(), '.markus', 'skills', name);
            if (existsSync(skillDir)) {
              rmSync(skillDir, { recursive: true, force: true });
              if (this.skillRegistry) {
                try { this.skillRegistry.unregister(name); } catch { /* skip */ }
              }
            }
          }

          this.json(res, 200, { uninstalled: true, type, name, removedAgents, removedTeamId });
        } catch (err) {
          this.json(res, 500, { error: `Uninstall failed: ${String(err)}` });
        }
        return;
      }
    }

    // DELETE /api/builder/artifacts/:type/:name — remove artifact
    {
      const delMatch = path.match(/^\/api\/builder\/artifacts\/(agents?|teams?|skills?)\/([^/]+)$/);
      if (delMatch && req.method === 'DELETE') {
        const rawType = delMatch[1]!;
        const name = decodeURIComponent(delMatch[2]!);
        const typeDir = rawType.endsWith('s') ? rawType : rawType + 's';
        const artDir = join(homedir(), '.markus', 'builder-artifacts', typeDir, name);
        if (!existsSync(artDir)) {
          this.json(res, 404, { error: 'Artifact not found' });
          return;
        }
        try {
          rmSync(artDir, { recursive: true, force: true });
          this.json(res, 200, { deleted: true, type: typeDir === 'agents' ? 'agent' : typeDir === 'teams' ? 'team' : 'skill', name });
        } catch (err) {
          this.json(res, 500, { error: `Delete failed: ${String(err)}` });
        }
        return;
      }
    }

    // POST /api/builder/artifacts/:type/:name/images — upload image
    {
      const imgPostMatch = path.match(/^\/api\/builder\/artifacts\/(agents?|teams?|skills?)\/([^/]+)\/images$/);
      if (imgPostMatch && req.method === 'POST') {
        const rawType = imgPostMatch[1]!;
        const name = decodeURIComponent(imgPostMatch[2]!);
        const typeDir = rawType.endsWith('s') ? rawType : rawType + 's';
        const type = typeDir === 'agents' ? 'agent' : typeDir === 'teams' ? 'team' : 'skill' as const;
        const artDir = join(homedir(), '.markus', 'builder-artifacts', typeDir, name);
        if (!existsSync(artDir)) { this.json(res, 404, { error: 'Artifact not found' }); return; }

        try {
          const imagesDir = join(artDir, 'images');
          if (!existsSync(imagesDir)) mkdirSync(imagesDir, { recursive: true });

          const chunks: Buffer[] = [];
          for await (const chunk of req) chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
          const body = Buffer.concat(chunks);

          const contentType = req.headers['content-type'] ?? '';
          if (contentType.includes('multipart/form-data')) {
            const boundary = contentType.split('boundary=')[1]?.split(';')[0];
            if (!boundary) { this.json(res, 400, { error: 'Missing boundary' }); return; }

            const bodyStr = body.toString('latin1');
            const parts = bodyStr.split('--' + boundary).filter(p => p.includes('Content-Disposition'));
            for (const part of parts) {
              const nameMatch = part.match(/filename="([^"]+)"/);
              if (!nameMatch) continue;
              const filename = nameMatch[1]!.replace(/[^a-zA-Z0-9._-]/g, '_');
              const headerEnd = part.indexOf('\r\n\r\n');
              if (headerEnd < 0) continue;
              const fileContent = part.slice(headerEnd + 4).replace(/\r\n$/, '').replace(/\r\n--$/, '');
              const filePath = join(imagesDir, filename);
              writeFileSync(filePath, Buffer.from(fileContent, 'latin1'));

              const manifestFile = join(artDir, `${type}.json`);
              if (existsSync(manifestFile)) {
                try {
                  const manifest = JSON.parse(readFileSync(manifestFile, 'utf-8'));
                  const screenshots: string[] = manifest.screenshots ?? [];
                  const relPath = `images/${filename}`;
                  if (!screenshots.includes(relPath)) {
                    screenshots.push(relPath);
                    manifest.screenshots = screenshots;
                    if (!manifest.thumbnail) manifest.thumbnail = relPath;
                    writeFileSync(manifestFile, JSON.stringify(manifest, null, 2));
                  }
                } catch { /* skip manifest update */ }
              }

              this.json(res, 200, { filename, path: `images/${filename}` });
              return;
            }
            this.json(res, 400, { error: 'No image file found in upload' });
          } else {
            this.json(res, 400, { error: 'Expected multipart/form-data' });
          }
        } catch (err) {
          this.json(res, 500, { error: `Upload failed: ${String(err)}` });
        }
        return;
      }
    }

    // GET /api/builder/artifacts/:type/:name/images/:filename — serve image
    {
      const imgGetMatch = path.match(/^\/api\/builder\/artifacts\/(agents?|teams?|skills?)\/([^/]+)\/images\/([^/]+)$/);
      if (imgGetMatch && req.method === 'GET') {
        const rawType = imgGetMatch[1]!;
        const name = decodeURIComponent(imgGetMatch[2]!);
        const filename = decodeURIComponent(imgGetMatch[3]!);
        const typeDir = rawType.endsWith('s') ? rawType : rawType + 's';
        const filePath = join(homedir(), '.markus', 'builder-artifacts', typeDir, name, 'images', filename);
        if (!existsSync(filePath)) { this.json(res, 404, { error: 'Image not found' }); return; }

        const ext = filename.split('.').pop()?.toLowerCase() ?? '';
        const mimeTypes: Record<string, string> = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp', svg: 'image/svg+xml' };
        res.writeHead(200, { 'Content-Type': mimeTypes[ext] ?? 'application/octet-stream', 'Cache-Control': 'public, max-age=3600' });
        res.end(readFileSync(filePath));
        return;
      }
    }

    // DELETE /api/builder/artifacts/:type/:name/images/:filename — remove image
    {
      const imgDelMatch = path.match(/^\/api\/builder\/artifacts\/(agents?|teams?|skills?)\/([^/]+)\/images\/([^/]+)$/);
      if (imgDelMatch && req.method === 'DELETE') {
        const rawType = imgDelMatch[1]!;
        const name = decodeURIComponent(imgDelMatch[2]!);
        const filename = decodeURIComponent(imgDelMatch[3]!);
        const typeDir = rawType.endsWith('s') ? rawType : rawType + 's';
        const type = typeDir === 'agents' ? 'agent' : typeDir === 'teams' ? 'team' : 'skill' as const;
        const artDir = join(homedir(), '.markus', 'builder-artifacts', typeDir, name);
        const filePath = join(artDir, 'images', filename);
        if (!existsSync(filePath)) { this.json(res, 404, { error: 'Image not found' }); return; }

        try {
          rmSync(filePath);
          const manifestFile = join(artDir, `${type}.json`);
          if (existsSync(manifestFile)) {
            try {
              const manifest = JSON.parse(readFileSync(manifestFile, 'utf-8'));
              const relPath = `images/${filename}`;
              if (Array.isArray(manifest.screenshots)) {
                manifest.screenshots = manifest.screenshots.filter((s: string) => s !== relPath);
              }
              if (manifest.thumbnail === relPath) {
                manifest.thumbnail = manifest.screenshots?.[0] ?? undefined;
              }
              writeFileSync(manifestFile, JSON.stringify(manifest, null, 2));
            } catch { /* skip */ }
          }
          this.json(res, 200, { deleted: true, filename });
        } catch (err) {
          this.json(res, 500, { error: `Delete failed: ${String(err)}` });
        }
        return;
      }
    }

    // Skills registry: SkillHub (skillhub.tencent.com) — static JSON from Tencent CDN
    if (path === '/api/skills/registry/skillhub' && req.method === 'GET') {
      const q = url.searchParams.get('q') ?? '';
      const category = url.searchParams.get('category') ?? '';
      const page = parseInt(url.searchParams.get('page') ?? '1', 10);
      const limit = parseInt(url.searchParams.get('limit') ?? '24', 10);
      const sort = url.searchParams.get('sort') ?? 'score';

      const cacheKey = 'skillhub-data';
      const now = Date.now();
      const cached = this.registryCache?.get(cacheKey) as { data: { total: number; generated_at: string; featured: string[]; categories: Record<string, string[]>; skills: Array<{ slug: string; name: string; description: string; description_zh?: string; version: string; homepage: string; tags: string[]; downloads: number; stars: number; installs: number; updated_at: number; score: number }> }; ts: number } | undefined;

      try {
        let allData = cached && now - cached.ts < 3_600_000 ? cached.data : null;
        if (!allData) {
          const dataUrl = 'https://cloudcache.tencentcs.com/qcloud/tea/app/data/skills.66a05e01.json';
          const resp = await fetch(dataUrl);
          if (!resp.ok) {
            this.json(res, 502, { error: `SkillHub CDN returned ${resp.status}` });
            return;
          }
          allData = await resp.json() as typeof cached extends undefined ? never : NonNullable<typeof cached>['data'];
          if (!this.registryCache) this.registryCache = new Map();
          this.registryCache.set(cacheKey, { data: allData, ts: now });
        }

        let skills = allData!.skills;
        const categoryMap = allData!.categories ?? {};

        if (category && categoryMap[category]) {
          const catTags = new Set(categoryMap[category]!.map(t => t.toLowerCase()));
          skills = skills.filter(s => s.tags?.some(t => catTags.has(t.toLowerCase())));
        }

        if (q) {
          const lower = q.toLowerCase();
          skills = skills.filter(s =>
            s.name.toLowerCase().includes(lower) ||
            s.slug.toLowerCase().includes(lower) ||
            (s.description_zh ?? s.description ?? '').toLowerCase().includes(lower)
          );
        }

        if (sort === 'downloads') skills.sort((a, b) => b.downloads - a.downloads);
        else if (sort === 'stars') skills.sort((a, b) => b.stars - a.stars);
        else if (sort === 'installs') skills.sort((a, b) => b.installs - a.installs);
        else skills.sort((a, b) => b.score - a.score);

        const total = skills.length;
        const start = (page - 1) * limit;
        const pageSkills = skills.slice(start, start + limit);

        // Enrich homepage URLs: CDN data often has `clawhub.ai/{slug}` instead of `clawhub.ai/{owner}/{slug}`
        if (!this.registryCache) this.registryCache = new Map();
        const ownerCacheKey = 'skillhub-owner-map';
        const ownerMap: Map<string, string> = (this.registryCache.get(ownerCacheKey) as { data: Map<string, string> } | undefined)?.data ?? new Map();
        const needsEnrich = pageSkills.filter(s => {
          const hp = s.homepage ?? '';
          const hpPath = hp.replace(/^https?:\/\/clawhub\.ai\/?/, '');
          return hpPath && !hpPath.includes('/') && !ownerMap.has(s.slug);
        });
        if (needsEnrich.length > 0) {
          await Promise.allSettled(
            needsEnrich.map(async s => {
              try {
                const resp = await fetch(`https://wry-manatee-359.convex.site/api/v1/skills/${encodeURIComponent(s.slug)}`, { signal: AbortSignal.timeout(5000) });
                if (resp.ok) {
                  const detail = await resp.json() as { owner?: { handle?: string } };
                  if (detail.owner?.handle) {
                    ownerMap.set(s.slug, detail.owner.handle);
                  }
                }
              } catch { /* skip — will use original homepage */ }
            })
          );
          this.registryCache.set(ownerCacheKey, { data: ownerMap, ts: now });
        }
        const enrichedSkills = pageSkills.map(s => {
          const hp = s.homepage ?? '';
          const hpPath = hp.replace(/^https?:\/\/clawhub\.ai\/?/, '');
          if (hpPath && !hpPath.includes('/') && ownerMap.has(s.slug)) {
            return { ...s, homepage: `https://clawhub.ai/${ownerMap.get(s.slug)}/${s.slug}` };
          }
          return s;
        });

        this.json(res, 200, {
          skills: enrichedSkills,
          total,
          page,
          limit,
          categories: Object.keys(categoryMap),
          featured: allData!.featured,
          cached: !!(cached && now - cached.ts < 3_600_000),
        });
      } catch (err) {
        this.json(res, 500, { error: `SkillHub fetch failed: ${String(err)}` });
      }
      return;
    }

    // Skills registry: Proxy fetch from skills.sh leaderboard
    if (path === '/api/skills/registry/skillssh' && req.method === 'GET') {
      const q = url.searchParams.get('q') ?? '';
      const cacheKey = `skillssh-${q || 'leaderboard'}`;
      const now = Date.now();
      const cached = this.registryCache?.get(cacheKey);
      if (cached && now - cached.ts < 600_000) {
        this.json(res, 200, { skills: cached.data, cached: true });
        return;
      }
      try {
        const fetchUrl = q
          ? `https://skills.sh/search?q=${encodeURIComponent(q)}`
          : 'https://skills.sh/';
        const resp = await fetch(fetchUrl);
        if (!resp.ok) {
          this.json(res, 502, { error: `skills.sh returned ${resp.status}` });
          return;
        }
        const html = await resp.text();
        const skills: Array<{ name: string; author: string; repo: string; installs: string; url: string; description?: string }> = [];
        const seen = new Set<string>();

        // Parse the leaderboard HTML: each skill is an <a> block with h3 (name), p (author/repo), span (installs)
        const blockRegex = /<a[^>]*href="\/([\w-]+\/[\w.-]+\/[\w][\w.-]*)"[^>]*>([\s\S]*?)<\/a>/g;
        const IGNORED_PREFIXES = new Set(['_next', 'static', 'api', 'assets', 'images', 'fonts', 'css', 'js']);
        let match: RegExpExecArray | null;
        while ((match = blockRegex.exec(html)) !== null) {
          const fullPath = match[1]!;
          const parts = fullPath.split('/');
          if (parts.length < 3) continue;
          if (IGNORED_PREFIXES.has(parts[0]!)) continue;

          const author = parts[0]!;
          const repo = `${parts[0]}/${parts[1]}`;
          const block = match[2]!;

          // Extract skill name from <h3>
          const nameMatch = block.match(/<h3[^>]*>(.*?)<\/h3>/);
          const name = nameMatch?.[1]?.trim() ?? parts[2]!;

          // Extract install count from last <span class="font-mono ...">
          const installMatches = [...block.matchAll(/<span[^>]*font-mono[^>]*>([\d.]+[KMB]?)<\/span>/g)];
          const installs = installMatches.length > 0 ? installMatches[installMatches.length - 1]![1] ?? '' : '';

          const key = `${author}/${repo}/${name}`;
          if (seen.has(key)) continue;
          seen.add(key);
          skills.push({ name, author, repo, installs, url: `https://skills.sh/${fullPath}` });
        }

        // Fetch descriptions for top skills in parallel (batch of first 20)
        const toFetch = skills.slice(0, 20).filter(s => !s.description);
        if (toFetch.length > 0) {
          const descResults = await Promise.allSettled(
            toFetch.map(async (s) => {
              const pageResp = await fetch(s.url, { signal: AbortSignal.timeout(8000) });
              if (!pageResp.ok) return { name: s.name, desc: '' };
              const pageHtml = await pageResp.text();
              const pMatch = pageHtml.match(/<p[^>]*class="[^"]*text-muted[^"]*"[^>]*>(.*?)<\/p>/);
              if (pMatch) return { name: s.name, desc: pMatch[1]!.replace(/<[^>]+>/g, '').trim() };
              const firstP = pageHtml.match(/<article[^>]*>[\s\S]*?<p[^>]*>(.*?)<\/p>/);
              if (firstP) return { name: s.name, desc: firstP[1]!.replace(/<[^>]+>/g, '').trim() };
              return { name: s.name, desc: '' };
            })
          );
          for (const r of descResults) {
            if (r.status === 'fulfilled' && r.value.desc) {
              const skill = skills.find(s => s.name === r.value.name);
              if (skill) skill.description = r.value.desc;
            }
          }
        }

        if (!this.registryCache) this.registryCache = new Map();
        this.registryCache.set(cacheKey, { data: skills, ts: now });
        this.json(res, 200, { skills, cached: false });
      } catch (err) {
        this.json(res, 500, { error: `skills.sh fetch failed: ${String(err)}` });
      }
      return;
    }

    // Agent Templates
    if (path === '/api/templates' && req.method === 'GET') {
      if (!this.templateRegistry) {
        this.json(res, 200, { templates: [] });
        return;
      }
      const source = url.searchParams.get('source') as
        | 'official'
        | 'community'
        | 'custom'
        | undefined;
      const category = url.searchParams.get('category') ?? undefined;
      const text = url.searchParams.get('q') ?? undefined;
      const result =
        source || category || text
          ? this.templateRegistry.search({ source: source ?? undefined, category, text })
          : { templates: this.templateRegistry.list(), total: this.templateRegistry.list().length };
      this.json(res, 200, result);
      return;
    }

    if (path.match(/^\/api\/templates\/[^/]+$/) && req.method === 'GET') {
      if (!this.templateRegistry) {
        this.json(res, 404, { error: 'Template registry not configured' });
        return;
      }
      const templateId = path.split('/')[3]!;
      const template = this.templateRegistry.get(templateId);
      if (!template) {
        this.json(res, 404, { error: `Template not found: ${templateId}` });
        return;
      }
      this.json(res, 200, { template });
      return;
    }

    if (path.match(/^\/api\/templates\/[^/]+\/files$/) && req.method === 'GET') {
      if (!this.templateRegistry) {
        this.json(res, 404, { error: 'Template registry not configured' });
        return;
      }
      const templateId = path.split('/')[3]!;
      const template = this.templateRegistry.get(templateId);
      if (!template) {
        this.json(res, 404, { error: `Template not found: ${templateId}` });
        return;
      }
      const { existsSync: ex, readFileSync: rf, readdirSync: rd } = await import('node:fs');
      const roleId = template.roleId;
      const candidates = [
        resolve(process.cwd(), 'templates', 'roles', roleId),
      ];
      try {
        const thisFile = (await import('node:url')).fileURLToPath(import.meta.url);
        const thisDir = (await import('node:path')).dirname(thisFile);
        candidates.unshift(resolve(thisDir, '..', 'templates', 'roles', roleId));
        candidates.push(resolve(thisDir, '..', '..', '..', '..', 'templates', 'roles', roleId));
      } catch { /* skip */ }
      const roleDir = candidates.find(d => ex(d));
      const files: Record<string, string> = {};
      if (roleDir) {
        try {
          for (const entry of rd(roleDir, { withFileTypes: true })) {
            if (entry.isFile() && !entry.name.endsWith('.json')) {
              try { files[entry.name] = rf(resolve(roleDir, entry.name), 'utf-8'); } catch { /* skip */ }
            }
          }
        } catch { /* skip */ }
      }
      this.json(res, 200, { files });
      return;
    }

    if (path === '/api/templates/instantiate' && req.method === 'POST') {
      const body = await this.readBody(req);
      const templateId = body['templateId'] as string;
      const name = body['name'] as string;
      const orgId = (body['orgId'] as string) ?? 'default';
      const teamId = body['teamId'] as string | undefined;
      const agentRole = body['agentRole'] as 'manager' | 'worker' | undefined;
      if (!templateId || !name) {
        this.json(res, 400, { error: 'templateId and name are required' });
        return;
      }
      try {
        const agentManager = this.orgService.getAgentManager();

        const agent = await agentManager.createAgentFromTemplate({
              templateId,
              name,
              orgId,
              teamId,
              overrides: body['overrides'] as Record<string, unknown> | undefined,
            });
        if (agentRole) agent.config.agentRole = agentRole;
        if (teamId) {
          this.orgService.addMemberToTeam(teamId, agent.id, 'agent');
        }

        // Persist to DB so agents survive restarts
        if (this.storage) {
          try {
            await this.storage.agentRepo.create({
              id: agent.id,
              name: agent.config.name,
              orgId,
              teamId,
              roleId: agent.config.roleId,
              roleName: agent.role.name,
              agentRole: agent.config.agentRole ?? 'worker',
              skills: agent.config.skills,
              llmConfig: agent.config.llmConfig,
              heartbeatIntervalMs: agent.config.heartbeatIntervalMs,
            });
          } catch (persistErr) {
            log.warn('Failed to persist instantiated agent to DB', { error: String(persistErr) });
          }
        }

        await agentManager.startAgent(agent.id);
        this.json(res, 201, {
          agent: {
            id: agent.id,
            name: agent.config.name,
            role: agent.role.name,
            agentRole: agent.config.agentRole,
            status: agent.getState().status,
          },
        });
      } catch (err) {
        this.json(res, 400, { error: String(err) });
      }
      return;
    }

    // ── External Agents ─────────────────────────────────────────────────────
    if (path === '/api/external-agents' && req.method === 'GET') {
      if (!this.gateway) {
        this.json(res, 200, { agents: [] });
        return;
      }
      const orgId = url.searchParams.get('orgId') ?? 'default';
      this.json(res, 200, { agents: this.gateway.listRegistrations(orgId) });
      return;
    }

    if (path === '/api/external-agents/register' && req.method === 'POST') {
      if (!this.gateway) {
        this.json(res, 503, { error: 'External agent gateway not configured' });
        return;
      }
      const body = await this.readBody(req);
      try {
        const orgId = (body['orgId'] as string) ?? 'default';
        const reg = await this.gateway.register({
          externalAgentId: body['externalAgentId'] as string,
          agentName: body['agentName'] as string,
          orgId,
          capabilities: body['capabilities'] as string[] | undefined,
          platform: body['platform'] as string | undefined,
          platformConfig: body['platformConfig'] as string | undefined,
          agentCardUrl: body['agentCardUrl'] as string | undefined,
          openClawConfig: body['openClawConfig'] as string | undefined,
        });
        // Generate a token for the UI without marking the agent as connected.
        // authenticate() sets connected=true as a side effect, so we reset it
        // immediately — the agent should only appear online when it actually syncs.
        let token: string | undefined;
        if (reg.markusAgentId && this.gatewaySecret) {
          try {
            const authResult = this.gateway.authenticate({
              externalAgentId: reg.externalAgentId,
              orgId,
              secret: this.gatewaySecret,
            });
            token = authResult.token;
            reg.connected = false;
            reg.lastHeartbeat = undefined;
            this.gateway.resetConnectionStatus(reg.externalAgentId, orgId);
          } catch { /* auth may fail if secret isn't set; token stays undefined */ }
        }
        const host = req.headers['host'] ?? `localhost:${this.port}`;
        const proto = req.headers['x-forwarded-proto'] ?? 'http';
        const gatewayUrl = `${proto}://${host}/api/gateway`;
        this.json(res, 201, { registration: reg, token, gatewayUrl });
      } catch (err) {
        const code = (err as { statusCode?: number }).statusCode ?? 400;
        this.json(res, code, { error: String(err) });
      }
      return;
    }

    if (path.match(/^\/api\/external-agents\/[^/]+$/) && req.method === 'DELETE') {
      if (!this.gateway) {
        this.json(res, 503, { error: 'External agent gateway not configured' });
        return;
      }
      const externalId = path.split('/')[3]!;
      const orgId = url.searchParams.get('orgId') ?? 'default';
      const reg = await this.gateway.unregister(externalId, orgId);
      if (reg?.markusAgentId) {
        try { await this.orgService.fireAgent(reg.markusAgentId); } catch { /* already gone */ }
      }
      this.json(res, reg ? 200 : 404, reg ? { deleted: true } : { error: 'Not found' });
      return;
    }

    // HITL: Approvals
    if (path === '/api/approvals' && req.method === 'GET') {
      const authUser = this.authEnabled
        ? await this.requireAuth(req, res)
        : await this.getAuthUser(req);
      if (!authUser) return;
      const status = url.searchParams.get('status') as
        | 'pending'
        | 'approved'
        | 'rejected'
        | undefined;
      let approvals = this.hitlService?.listApprovals(status ?? undefined) ?? [];
      const isPrivileged = authUser.role === 'owner' || authUser.role === 'admin';
      if (!isPrivileged) {
        approvals = approvals.filter(a => {
          if (a.approverUserIds?.length) {
            return a.approverUserIds.includes(authUser.userId);
          }
          if (a.targetUserId && a.targetUserId !== 'all') {
            return a.targetUserId === authUser.userId;
          }
          return true;
        });
      }
      this.json(res, 200, { approvals });
      return;
    }

    if (path === '/api/approvals' && req.method === 'POST') {
      if (!this.hitlService) {
        this.json(res, 503, { error: 'HITL service not available' });
        return;
      }
      const body = await this.readBody(req);
      const rawApprovers = body['approverUserIds'];
      const approverUserIds = Array.isArray(rawApprovers)
        ? rawApprovers.map((x: unknown) => String(x))
        : undefined;
      const approval = this.hitlService.requestApproval({
        agentId: body['agentId'] as string,
        agentName: (body['agentName'] as string) ?? 'Agent',
        type: (body['type'] as 'action' | 'custom') ?? 'custom',
        title: body['title'] as string,
        description: body['description'] as string,
        details: body['details'] as Record<string, unknown>,
        targetUserId: body['targetUserId'] as string,
        approverUserIds,
      });
      this.json(res, 201, { approval });
      return;
    }

    if (path.startsWith('/api/approvals/') && req.method === 'POST') {
      if (!this.hitlService) {
        this.json(res, 503, { error: 'HITL service not available' });
        return;
      }
      const authUser = this.authEnabled
        ? await this.requireAuth(req, res)
        : await this.getAuthUser(req);
      if (!authUser) return;
      const approvalId = path.split('/')[3]!;
      const pending = this.hitlService.getApproval(approvalId);
      if (!pending || pending.status !== 'pending') {
        this.json(res, 404, { error: 'Approval not found or not pending' });
        return;
      }
      const isPrivileged = authUser.role === 'owner' || authUser.role === 'admin';
      const approvers = pending.approverUserIds;
      const canRespond =
        isPrivileged ||
        !approvers ||
        approvers.length === 0 ||
        approvers.includes(authUser.userId);
      if (!canRespond) {
        this.json(res, 403, { error: 'Not authorized to respond to this approval' });
        return;
      }
      const body = await this.readBody(req);
      const approved = body['approved'] as boolean;
      const respondedBy = this.authEnabled
        ? authUser.userId
        : (body['respondedBy'] as string) ?? authUser.userId ?? 'anonymous';
      const comment = body['comment'] as string | undefined;
      const selectedOption = body['selectedOption'] as string | undefined;
      const result = this.hitlService.respondToApproval(
        approvalId, approved, respondedBy, comment, selectedOption,
      );
      if (!result) {
        this.json(res, 404, { error: 'Approval not found or not pending' });
        return;
      }

      // Directly transition task/requirement as fallback.
      // The in-memory promise callback from requestApprovalAndWait is lost on
      // server restart, so we must also drive the state change from here.
      const details = result.details as Record<string, unknown> | undefined;
      const taskId = details?.['taskId'] as string | undefined;
      const requirementId = details?.['requirementId'] as string | undefined;
      if (taskId) {
        try {
          if (approved) {
            this.taskService.approveTask(taskId, respondedBy);
          } else {
            this.taskService.rejectTask(taskId, respondedBy);
          }
        } catch { /* already transitioned by in-memory promise callback */ }
      }
      if (requirementId && this.requirementService) {
        try {
          if (approved) {
            this.requirementService.approveRequirement(requirementId, respondedBy);
          } else {
            this.requirementService.rejectRequirement(requirementId, respondedBy, comment || 'Rejected via approval');
          }
        } catch { /* already transitioned by in-memory promise callback */ }
      }

      this.auditService?.record({
        orgId: 'default',
        userId: respondedBy,
        agentId: result.agentId,
        type: 'approval_response',
        action: approved ? 'approve' : 'reject',
        detail: result.title,
        success: approved,
        metadata: { approvalId, taskId, requirementId, comment, selectedOption },
      });

      this.json(res, 200, { approval: result });
      return;
    }

    // Notifications
    if (path === '/api/notifications' && req.method === 'GET') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const userId = authUser.userId;
      const unread = url.searchParams.get('unread') === 'true';
      const type = url.searchParams.get('type') ?? undefined;
      const limit = parseInt(url.searchParams.get('limit') ?? '50', 10);
      const offset = parseInt(url.searchParams.get('offset') ?? '0', 10);
      const notifications = this.hitlService?.listNotifications(userId, unread, { type, limit, offset }) ?? [];
      const counts = this.hitlService?.countNotifications(userId) ?? { total: 0, unread: 0 };
      this.json(res, 200, {
        notifications,
        totalCount: counts.total,
        unreadCount: counts.unread,
      });
      return;
    }

    if (path === '/api/notifications/mark-all-read' && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const userId = authUser.userId;
      const count = this.hitlService?.markAllNotificationsRead(userId) ?? 0;
      this.json(res, 200, { success: true, count });
      return;
    }

    if (path.startsWith('/api/notifications/') && path.endsWith('/read') && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const notifId = path.split('/')[3]!;
      const read = this.hitlService?.markNotificationRead(notifId);
      this.json(res, 200, { success: read ?? false });
      return;
    }

    if (path.startsWith('/api/notifications/') && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const notifId = path.split('/')[3]!;
      const read = this.hitlService?.markNotificationRead(notifId);
      this.json(res, 200, { success: read ?? false });
      return;
    }

    // ── Unread message tracking ──────────────────────────────────────────────
    if (path === '/api/unread' && req.method === 'GET') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const repo = this.storage?.readCursorRepo;
      if (!repo) { this.json(res, 200, { counts: {} }); return; }
      const counts = repo.getUnreadCounts(authUser.userId);
      this.json(res, 200, { counts });
      return;
    }

    if (path === '/api/unread/mark-read' && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const body = await this.readBody(req);
      const { conversationKey, lastReadAt, lastReadId } = body as { conversationKey: string; lastReadAt: string; lastReadId?: string };
      if (!conversationKey || !lastReadAt) { this.json(res, 400, { error: 'conversationKey and lastReadAt required' }); return; }
      const repo = this.storage?.readCursorRepo;
      if (!repo) { this.json(res, 200, { success: true }); return; }
      repo.setReadCursor(authUser.userId, conversationKey, lastReadAt, lastReadId);
      this.json(res, 200, { success: true });
      return;
    }

    if (path === '/api/unread/mark-all-read' && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const repo = this.storage?.readCursorRepo;
      if (!repo) { this.json(res, 200, { success: true }); return; }
      repo.markAllRead(authUser.userId);
      this.json(res, 200, { success: true });
      return;
    }

    // Unified activity feed — merges notifications, task comments, and deliverables
    if (path === '/api/activity' && req.method === 'GET') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const userId = authUser.userId;
      const limit = Math.min(parseInt(url.searchParams.get('limit') ?? '50', 10), 200);
      const typeFilter = url.searchParams.get('type') ?? undefined;

      interface ActivityItem {
        id: string;
        type: string;
        title: string;
        body: string;
        timestamp: string;
        source: 'notification' | 'task_comment' | 'deliverable';
        metadata?: Record<string, unknown>;
      }

      const items: ActivityItem[] = [];

      // 1. Notifications
      if (!typeFilter || typeFilter === 'notification') {
        const notifications = this.hitlService?.listNotifications(userId, false, { limit }) ?? [];
        for (const n of notifications) {
          items.push({
            id: n.id,
            type: n.type,
            title: n.title,
            body: n.body,
            timestamp: n.createdAt,
            source: 'notification',
            metadata: n.metadata as Record<string, unknown> | undefined,
          });
        }
      }

      // 2. Recent task comments
      if ((!typeFilter || typeFilter === 'task_comment') && this.storage?.taskCommentRepo) {
        try {
          const recentComments = this.storage.taskCommentRepo.listRecent?.(limit) ?? [];
          for (const c of recentComments) {
            items.push({
              id: c.id,
              type: 'task_comment',
              title: `Comment on task ${c.taskId}`,
              body: typeof c.body === 'string' ? c.body.slice(0, 300) : String(c.body ?? ''),
              timestamp: c.createdAt,
              source: 'task_comment',
              metadata: { taskId: c.taskId, authorId: c.authorId, authorName: c.authorName },
            });
          }
        } catch { /* listRecent may not exist yet */ }
      }

      // Filter out items belonging to archived tasks
      const filtered = items.filter(item => {
        const taskId = (item.metadata as Record<string, unknown> | undefined)?.taskId as string | undefined;
        if (!taskId) return true;
        const task = this.taskService.getTask(taskId);
        return !task || task.status !== 'archived';
      });

      // Sort by timestamp descending, limit
      filtered.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
      const page = filtered.slice(0, limit);

      this.json(res, 200, { items: page, totalCount: filtered.length });
      return;
    }

    // Billing: Usage — computed from persisted agent metrics for restart-safety
    if (path === '/api/usage' && req.method === 'GET') {
      const orgId = url.searchParams.get('orgId') ?? 'default';
      const plan = this.billingService?.getOrgPlan(orgId);

      const agentManager = this.orgService.getAgentManager();
      const allAgents = agentManager.listAgents();
      let llmTokens = 0;
      let toolCalls = 0;
      let messages = 0;

      for (const a of allAgents) {
        try {
          const agent = agentManager.getAgent(a.id);
          const stats = agent.getUsageStats();
          llmTokens += stats.totalTokens;
          toolCalls += stats.toolCallsToday;
          messages += stats.requestsToday;
        } catch { /* agent not loaded */ }
      }

      this.json(res, 200, {
        usage: {
          orgId,
          period: new Date().toISOString().slice(0, 7),
          llmTokens,
          toolCalls,
          messages,
          storageBytes: this.billingService?.getUsageSummary(orgId)?.storageBytes ?? 0,
        },
        plan,
      });
      return;
    }

    // Billing: Per-agent usage — computed from persisted agent metrics
    if (path === '/api/usage/agents' && req.method === 'GET') {
      const agentManager = this.orgService.getAgentManager();
      const agentList = agentManager.listAgents();

      const agentUsage = agentList.map(a => {
        try {
          const agent = agentManager.getAgent(a.id);
          const stats = agent.getUsageStats();
          return {
            agentId: a.id,
            agentName: a.name,
            role: a.role,
            status: a.status,
            tokensUsedToday: stats.tokensToday,
            totalTokens: stats.totalTokens,
            promptTokens: stats.promptTokens,
            completionTokens: stats.completionTokens,
            requestCount: stats.requestCount,
            toolCalls: stats.toolCalls,
            messages: stats.requestsToday,
            estimatedCost: stats.estimatedCost,
            costToday: stats.costToday,
          };
        } catch {
          return {
            agentId: a.id,
            agentName: a.name,
            role: a.role,
            status: a.status,
            tokensUsedToday: 0,
            totalTokens: 0,
            promptTokens: 0,
            completionTokens: 0,
            requestCount: 0,
            toolCalls: 0,
            messages: 0,
            estimatedCost: 0,
            costToday: 0,
          };
        }
      });

      this.json(res, 200, { agents: agentUsage });
      return;
    }

    // Billing: API Keys
    if (path === '/api/keys' && req.method === 'GET') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const orgId = url.searchParams.get('orgId') ?? 'default';
      this.json(res, 200, { keys: this.billingService?.listAPIKeys(orgId) ?? [] });
      return;
    }

    if (path === '/api/keys' && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      if (!this.billingService) {
        this.json(res, 503, { error: 'Billing service not available' });
        return;
      }
      const body = await this.readBody(req);
      const key = this.billingService.createAPIKey(
        (body['orgId'] as string) ?? 'default',
        (body['name'] as string) ?? 'Default Key',
        body['scopes'] as string[],
        body['expiresInDays'] as number | undefined
      );
      this.json(res, 201, { key });
      return;
    }

    if (path.startsWith('/api/keys/') && req.method === 'DELETE') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const keyId = path.split('/')[3]!;
      const revoked = this.billingService?.revokeAPIKey(keyId);
      this.json(res, 200, { revoked: revoked ?? false });
      return;
    }

    // Billing: Plan
    if (path === '/api/plan' && req.method === 'GET') {
      const orgId = url.searchParams.get('orgId') ?? 'default';
      this.json(res, 200, { plan: this.billingService?.getOrgPlan(orgId) });
      return;
    }

    if (path === '/api/plan' && req.method === 'POST') {
      if (!this.billingService) {
        this.json(res, 503, { error: 'Billing service not available' });
        return;
      }
      const body = await this.readBody(req);
      const plan = this.billingService.setOrgPlan(
        (body['orgId'] as string) ?? 'default',
        (body['tier'] as 'free' | 'pro' | 'enterprise') ?? 'free'
      );
      this.json(res, 200, { plan });
      return;
    }

    // Team Templates
    if (path === '/api/templates/teams' && req.method === 'GET') {
      try {
        const { readdirSync, readFileSync } = await import('node:fs');
        const { resolve } = await import('node:path');
        const teamsDir = resolve(process.cwd(), 'templates', 'teams');
        const files = readdirSync(teamsDir).filter(f => f.endsWith('.json'));
        const teams = files.map(f => JSON.parse(readFileSync(resolve(teamsDir, f), 'utf-8')));
        this.json(res, 200, { templates: teams });
      } catch {
        this.json(res, 200, { templates: [] });
      }
      return;
    }

    // Audit log
    if (path === '/api/audit' && req.method === 'GET') {
      if (!this.auditService) {
        this.json(res, 200, { entries: [] });
        return;
      }
      const entries = this.auditService.query({
        orgId: url.searchParams.get('orgId') ?? 'default',
        agentId: url.searchParams.get('agentId') ?? undefined,
        type: (url.searchParams.get('type') as AuditEventType) ?? undefined,
        limit: url.searchParams.has('limit') ? Number(url.searchParams.get('limit')) : 50,
        since: url.searchParams.get('since') ?? undefined,
      });
      this.json(res, 200, { entries });
      return;
    }

    if (path === '/api/audit/summary' && req.method === 'GET') {
      if (!this.auditService) {
        this.json(res, 200, { summary: null });
        return;
      }
      const orgId = url.searchParams.get('orgId') ?? 'default';
      const summary = this.auditService.summary(orgId);
      this.json(res, 200, { summary });
      return;
    }

    if (path === '/api/audit/tokens' && req.method === 'GET') {
      if (!this.auditService) {
        this.json(res, 200, { usage: [] });
        return;
      }
      const usage = this.auditService.getTokenUsage(
        url.searchParams.get('orgId') ?? undefined,
        url.searchParams.get('agentId') ?? undefined
      );
      this.json(res, 200, { usage });
      return;
    }

    // ── Hub Proxy ─────────────────────────────────────────────────────────────
    if (path === '/api/hub/publish' && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const body = await this.readBody(req);
      const hubUrl = (body['hubUrl'] as string) ?? this.hubUrl;
      const hubToken = body['hubToken'] as string | undefined;
      if (!hubToken) {
        this.json(res, 401, { error: 'Hub token required. Please login to Markus Hub first.' });
        return;
      }
      try {
        const headers: Record<string, string> = {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${hubToken}`,
        };
        let hubRes = await fetch(`${hubUrl}/api/items`, {
          method: 'POST',
          headers,
          body: JSON.stringify(body['payload']),
          redirect: 'manual',
        });
        if (hubRes.status >= 300 && hubRes.status < 400) {
          const location = hubRes.headers.get('location');
          if (location) {
            hubRes = await fetch(location, { method: 'POST', headers, body: JSON.stringify(body['payload']), redirect: 'manual' });
          }
        }
        const hubData = await hubRes.json();
        this.json(res, hubRes.status, hubData);
      } catch (err) {
        this.json(res, 502, { error: `Hub request failed: ${String(err)}` });
      }
      return;
    }

    // Generic Hub API proxy (avoids CORS issues with cross-origin fetch to markus.global)
    if (path.startsWith('/api/hub/')) {
      const hubPath = path.slice('/api/hub'.length);
      const reqUrl = new URL(req.url!, `http://${req.headers.host}`);
      const hubTargetUrl = `${this.hubUrl}/api${hubPath}${reqUrl.search}`;

      const ct = String(req.headers['content-type'] ?? '').toLowerCase();
      const isMultipart = ct.includes('multipart/form-data');

      const proxyHeaders: Record<string, string> = {};
      if (!isMultipart) proxyHeaders['Content-Type'] = 'application/json';
      else proxyHeaders['Content-Type'] = req.headers['content-type']!;
      const authHeader = req.headers['authorization'];
      if (authHeader) proxyHeaders['Authorization'] = authHeader;

      try {
        let body: string | Buffer | undefined;
        if (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') {
          if (isMultipart) {
            const chunks: Buffer[] = [];
            for await (const chunk of req) chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
            body = Buffer.concat(chunks);
          } else {
            body = JSON.stringify(await this.readBody(req));
          }
        }
        let hubRes = await fetch(hubTargetUrl, {
          method: req.method,
          headers: proxyHeaders,
          body,
          redirect: 'manual',
        });
        if (hubRes.status >= 300 && hubRes.status < 400) {
          const location = hubRes.headers.get('location');
          if (location) {
            hubRes = await fetch(location, {
              method: req.method,
              headers: proxyHeaders,
              body,
              redirect: 'manual',
            });
          }
        }
        const data = await hubRes.json();
        this.json(res, hubRes.status, data);
      } catch (err) {
        this.json(res, 502, { error: `Hub request failed: ${String(err)}` });
      }
      return;
    }

    // Settings — Hub URL (for web-ui to discover hub address)
    if (path === '/api/settings/hub' && req.method === 'GET') {
      this.json(res, 200, { hubUrl: this.hubUrl });
      return;
    }

    // Settings — Hub Token (frontend pushes token so MCP skill servers can read it)
    if (path === '/api/settings/hub-token' && req.method === 'POST') {
      const body = await this.readBody(req);
      const authUser = await this.getAuthUser(req);
      const token = body['token'] as string | null;
      const tokenPath = join(homedir(), '.markus', 'hub-token');
      try {
        if (token) {
          mkdirSync(join(homedir(), '.markus'), { recursive: true });
          writeFileSync(tokenPath, token, 'utf-8');
        } else if (existsSync(tokenPath)) {
          rmSync(tokenPath);
        }
        log.info(`Hub token ${token ? 'saved to' : 'cleared from'} ${tokenPath}`);
      } catch (err) {
        log.error('Failed to write hub token file', { error: String(err) });
      }
      this.auditService?.record({
        orgId: 'system',
        type: 'settings_changed',
        action: 'hub_token',
        detail: token ? 'Hub token saved' : 'Hub token cleared',
        userId: authUser?.userId,
        success: true,
      });
      this.json(res, 200, { ok: true });
      return;
    }

    // Settings — Remote Access
    if (path === '/api/settings/remote' && req.method === 'GET') {
      const status = this.remoteAgent?.getStatus() ?? { enabled: false, connected: false, instanceId: null, remoteUrl: null, signalUrl: null, peerCount: 0 };
      this.json(res, 200, status);
      return;
    }

    if (path === '/api/settings/remote/enable' && req.method === 'POST') {
      if (!this.remoteAgent && this.remoteAgentFactory) {
        const agent = await this.remoteAgentFactory();
        if (agent) this.setRemoteAgent(agent);
      }
      if (!this.remoteAgent) {
        this.json(res, 400, { error: 'Remote access not configured. Please sign in to Markus Hub first.' });
        return;
      }
      this.remoteAgent.start().catch(() => {});
      this.json(res, 200, { ok: true, status: this.remoteAgent.getStatus() });
      return;
    }

    if (path === '/api/settings/remote/disable' && req.method === 'POST') {
      if (this.remoteAgent) {
        await this.remoteAgent.stop();
      }
      this.json(res, 200, { ok: true });
      return;
    }

    // Settings — LLM configuration
    if (path === '/api/settings/llm' && req.method === 'GET') {
      if (!this.llmRouter) {
        this.json(res, 200, { defaultProvider: 'unknown', providers: {} });
        return;
      }
      this.json(res, 200, this.llmRouter.getEnhancedSettings());
      return;
    }

    if (path === '/api/settings/llm' && req.method === 'POST') {
      const auth = await this.requireAuth(req, res);
      if (!auth) return;
      if (!this.llmRouter) {
        this.json(res, 503, { error: 'LLM router not available' });
        return;
      }
      const body = await this.readBody(req);
      const { defaultProvider, autoFallback } = body as { defaultProvider?: string; autoFallback?: boolean };
      if (!defaultProvider && autoFallback === undefined) {
        this.json(res, 400, { error: 'defaultProvider or autoFallback is required' });
        return;
      }
      try {
        const configUpdates: Record<string, unknown> = {};
        if (defaultProvider) {
          this.llmRouter.setDefaultProvider(defaultProvider);
          configUpdates.defaultProvider = defaultProvider;
        }
        if (typeof autoFallback === 'boolean') {
          this.llmRouter.setAutoFallback(autoFallback);
          configUpdates.autoFallback = autoFallback;
        }
        try {
          saveConfig({ llm: configUpdates } as any, this.markusConfigPath);
        } catch (e) {
          log.warn('Failed to persist LLM settings to config file', { error: String(e) });
        }
        this.auditService?.record({
          orgId: 'system',
          type: 'settings_changed',
          action: 'llm_settings',
          detail: `${defaultProvider ? `defaultProvider=${defaultProvider}` : ''}${autoFallback !== undefined ? ` autoFallback=${autoFallback}` : ''}`.trim(),
          userId: auth.userId,
          success: true,
        });
        this.json(res, 200, this.llmRouter.getEnhancedSettings());
      } catch (err) {
        this.json(res, 400, { error: String(err) });
      }
      return;
    }

    // Settings — Agent configuration (maxToolIterations, cognitive etc.)
    if (path === '/api/settings/agent' && req.method === 'GET') {
      const am = this.orgService.getAgentManager();
      this.json(res, 200, {
        maxToolIterations: am.maxToolIterations,
        cognitive: am.cognitiveConfig ?? { enabled: false },
      });
      return;
    }

    if (path === '/api/settings/agent' && req.method === 'POST') {
      const auth = await this.requireAuth(req, res);
      if (!auth) return;
      const body = await this.readBody(req);
      const am = this.orgService.getAgentManager();
      let changed = false;
      if (typeof body['maxToolIterations'] === 'number') {
        am.maxToolIterations = body['maxToolIterations'];
        changed = true;
      }
      if (body['cognitive'] && typeof body['cognitive'] === 'object') {
        const cc = body['cognitive'] as Record<string, unknown>;
        am.cognitiveConfig = {
          enabled: cc['enabled'] === true,
          maxDepth: typeof cc['maxDepth'] === 'number' ? cc['maxDepth'] : undefined,
          appraisalModel: typeof cc['appraisalModel'] === 'string' ? cc['appraisalModel'] : undefined,
          timeoutMs: typeof cc['timeoutMs'] === 'number' ? cc['timeoutMs'] : undefined,
        };
        changed = true;
      }
      if (changed) {
        try {
          saveConfig({ agent: { maxToolIterations: am.maxToolIterations } } as any, this.markusConfigPath);
        } catch (e) {
          log.warn('Failed to persist agent settings to config file', { error: String(e) });
        }
        for (const info of am.listAgents()) {
          const agent = am.getAgent(info.id);
          if (agent) agent.maxToolIterations = am.maxToolIterations;
        }
        this.auditService?.record({
          orgId: 'system',
          type: 'settings_changed',
          action: 'agent_max_tool_iterations',
          detail: `maxToolIterations=${am.maxToolIterations}`,
          userId: auth.userId,
          success: true,
        });
      }
      this.json(res, 200, {
        maxToolIterations: am.maxToolIterations,
        cognitive: am.cognitiveConfig ?? { enabled: false },
      });
      return;
    }

    // Settings — Browser automation
    if (path === '/api/settings/browser' && req.method === 'GET') {
      const { loadConfig: loadCfg } = await import('@markus/shared');
      const currentConfig = loadCfg(this.markusConfigPath);
      const browser = currentConfig.browser ?? {};
      const am = this.orgService.getAgentManager();
      this.json(res, 200, {
        bringToFront: browser.bringToFront ?? false,
        remoteDebuggingPort: browser.remoteDebuggingPort ?? 0,
        autoCloseTabs: browser.autoCloseTabs ?? true,
        autoClickAllowDialog: browser.autoClickAllowDialog ?? false,
        extensionBridgePort: browser.extensionBridgePort ?? 9333,
        extensionConnected: am.browserExtensionConnected,
      });
      return;
    }

    if (path === '/api/settings/browser' && req.method === 'POST') {
      const auth = await this.requireAuth(req, res);
      if (!auth) return;
      const body = await this.readBody(req);
      const updates: Record<string, unknown> = {};
      if (typeof body['bringToFront'] === 'boolean') updates.bringToFront = body['bringToFront'];
      if (typeof body['remoteDebuggingPort'] === 'number') updates.remoteDebuggingPort = body['remoteDebuggingPort'];
      if (typeof body['autoCloseTabs'] === 'boolean') updates.autoCloseTabs = body['autoCloseTabs'];
      if (typeof body['autoClickAllowDialog'] === 'boolean') updates.autoClickAllowDialog = body['autoClickAllowDialog'];
      try {
        saveConfig({ browser: updates } as any, this.markusConfigPath);
        const am = this.orgService.getAgentManager();
        if (typeof updates.bringToFront === 'boolean') {
          am.setBrowserBringToFront(updates.bringToFront);
        }
        if (typeof updates.autoCloseTabs === 'boolean') {
          am.setBrowserAutoCloseTabs(updates.autoCloseTabs);
        }
        if (typeof updates.remoteDebuggingPort === 'number') {
          am.setBrowserRemoteDebuggingPort(updates.remoteDebuggingPort);
        }
        if (typeof updates.autoClickAllowDialog === 'boolean') {
          am.setBrowserAutoClickAllowDialog(updates.autoClickAllowDialog);
        }
      } catch (e) {
        log.warn('Failed to persist browser settings', { error: String(e) });
      }
      if (Object.keys(updates).length > 0) {
        this.auditService?.record({
          orgId: 'system',
          type: 'settings_changed',
          action: 'browser',
          detail: 'Browser automation settings updated',
          userId: auth.userId,
          success: true,
          metadata: { updates },
        });
      }
      const { loadConfig: loadCfg } = await import('@markus/shared');
      const currentConfig = loadCfg(this.markusConfigPath);
      const browser = currentConfig.browser ?? {};
      const am2 = this.orgService.getAgentManager();
      this.json(res, 200, {
        bringToFront: browser.bringToFront ?? false,
        remoteDebuggingPort: browser.remoteDebuggingPort ?? 0,
        autoCloseTabs: browser.autoCloseTabs ?? true,
        autoClickAllowDialog: browser.autoClickAllowDialog ?? false,
        extensionBridgePort: browser.extensionBridgePort ?? 9333,
        extensionConnected: am2.browserExtensionConnected,
      });
      return;
    }

    // Settings — Chrome Extension: download zip
    if (path === '/api/settings/browser/extension.zip' && req.method === 'GET') {
      const auth = await this.requireAuth(req, res);
      if (!auth) return;

      try {
        const { fileURLToPath } = await import('node:url');
        const { dirname: dn, resolve: rslv, join: jn } = await import('node:path');
        const { execSync } = await import('node:child_process');
        const { existsSync: ex, readFileSync, statSync } = await import('node:fs');

        const thisDir = dn(fileURLToPath(import.meta.url));
        // Search order: dev workspace → binary install → cwd fallback
        const zipCandidates = [
          jn(rslv(thisDir, '..', '..', 'chrome-extension'), 'dist', 'markus-browser-extension.zip'),
          jn(rslv(thisDir, '..', '..', '..', 'chrome-extension'), 'markus-browser-extension.zip'),
          jn(rslv(process.cwd(), 'packages', 'chrome-extension'), 'dist', 'markus-browser-extension.zip'),
          jn(rslv(process.cwd(), 'chrome-extension'), 'markus-browser-extension.zip'),
        ];
        let zipPath = zipCandidates.find(p => ex(p));

        // If not found, try building from source
        if (!zipPath) {
          const extDir = [
            rslv(thisDir, '..', '..', 'chrome-extension'),
            rslv(process.cwd(), 'packages', 'chrome-extension'),
          ].find(d => ex(jn(d, 'package.json')));
          if (extDir) {
            try { execSync('pnpm run pack', { cwd: extDir, timeout: 30000, stdio: 'pipe' }); } catch { /* ignore */ }
            const built = jn(extDir, 'dist', 'markus-browser-extension.zip');
            if (ex(built)) zipPath = built;
          }
        }
        if (!zipPath) { this.json(res, 404, { error: 'Extension zip not found.' }); return; }

        const data = readFileSync(zipPath);
        res.writeHead(200, {
          'Content-Type': 'application/zip',
          'Content-Disposition': 'attachment; filename="markus-browser-extension.zip"',
          'Content-Length': data.length,
        });
        res.end(data);
      } catch (e) {
        this.json(res, 500, { error: String(e) });
      }
      return;
    }

    // Settings — Chrome Extension: open chrome://extensions page
    if (path === '/api/settings/browser/open-extensions-page' && req.method === 'POST') {
      const auth = await this.requireAuth(req, res);
      if (!auth) return;

      try {
        const { exec: execCb } = await import('node:child_process');
        const platform = process.platform;
        if (platform === 'darwin') {
          execCb('open -a "Google Chrome" "chrome://extensions"', () => {});
        } else if (platform === 'win32') {
          execCb('start chrome "chrome://extensions"', () => {});
        } else {
          execCb('xdg-open "chrome://extensions" 2>/dev/null || google-chrome "chrome://extensions"', () => {});
        }
        this.json(res, 200, { ok: true });
      } catch (e) {
        this.json(res, 500, { error: String(e) });
      }
      return;
    }

    // Settings — Browser auto-click test
    if (path === '/api/settings/browser/test-auto-click' && req.method === 'POST') {
      const auth = await this.requireAuth(req, res);
      if (!auth) return;
      const { testAutoClick } = await import('@markus/core');
      const result = await testAutoClick();
      this.json(res, 200, result);
      return;
    }

    // Settings — Browser concurrent integration test
    if (path === '/api/settings/browser/test-concurrent' && req.method === 'POST') {
      const auth = await this.requireAuth(req, res);
      if (!auth) return;
      const am = this.orgService.getAgentManager();
      const params = await this.readBody(req).catch(() => ({} as Record<string, unknown>));
      const mode = (params.mode as string) ?? 'quick';

      if (mode === 'chaos') {
        const durationMs = Math.min(((params.durationSec as number) ?? 120) * 1000, 600_000);
        const agentCount = Math.min((params.agents as number) ?? 3, 5);

        res.writeHead(200, {
          'Content-Type': 'text/event-stream',
          'Cache-Control': 'no-cache',
          'Connection': 'keep-alive',
          'X-Accel-Buffering': 'no',
        });

        const ac = new AbortController();
        const chaosAbortKey = '__chaos_abort__';
        (this as unknown as Record<string, AbortController>)[chaosAbortKey] = ac;

        req.on('close', () => ac.abort());

        try {
          const gen = am.runChaosBrowserTest({ durationMs, agentCount, signal: ac.signal });
          for await (const ev of gen) {
            if (ac.signal.aborted) break;
            res.write(`event: ${ev.type}\ndata: ${JSON.stringify(ev)}\n\n`);
          }
        } catch (err) {
          const msg = err instanceof Error ? err.message : String(err);
          res.write(`event: error\ndata: ${JSON.stringify({ error: msg })}\n\n`);
        }
        res.end();
        delete (this as unknown as Record<string, AbortController>)[chaosAbortKey];
        return;
      }

      // Quick mode (default)
      const result = await am.runQuickBrowserTest();
      this.json(res, 200, result);
      return;
    }

    // Settings — Stop chaos test
    if (path === '/api/settings/browser/test-concurrent' && req.method === 'DELETE') {
      const auth = await this.requireAuth(req, res);
      if (!auth) return;
      const chaosAbortKey = '__chaos_abort__';
      const ac = (this as unknown as Record<string, AbortController>)[chaosAbortKey];
      if (ac) {
        ac.abort();
        delete (this as unknown as Record<string, AbortController>)[chaosAbortKey];
      }
      this.json(res, 200, { ok: true });
      return;
    }

    // Settings — Search API keys
    if (path === '/api/settings/search' && req.method === 'GET') {
      const { loadConfig: loadCfg } = await import('@markus/shared');
      const currentConfig = loadCfg(this.markusConfigPath);
      const search = currentConfig.integrations?.search ?? {};
      const mask = (key?: string) => key ? '***' + key.slice(-4) : '';
      this.json(res, 200, {
        serper: { configured: !!search.serperApiKey || !!process.env['SERPER_API_KEY'], preview: mask(search.serperApiKey || process.env['SERPER_API_KEY']) },
        brave: { configured: !!search.braveApiKey || !!process.env['BRAVE_SEARCH_API_KEY'], preview: mask(search.braveApiKey || process.env['BRAVE_SEARCH_API_KEY']) },
        bocha: { configured: !!search.bochaApiKey || !!process.env['BOCHA_API_KEY'], preview: mask(search.bochaApiKey || process.env['BOCHA_API_KEY']) },
      });
      return;
    }

    if (path === '/api/settings/search' && req.method === 'POST') {
      const auth = await this.requireAuth(req, res);
      if (!auth) return;
      const body = await this.readBody(req);
      const updates: Record<string, string | undefined> = {};
      if (typeof body['serperApiKey'] === 'string') {
        updates.serperApiKey = body['serperApiKey'] || undefined;
        if (body['serperApiKey']) process.env['SERPER_API_KEY'] = body['serperApiKey'] as string;
        else delete process.env['SERPER_API_KEY'];
      }
      if (typeof body['braveApiKey'] === 'string') {
        updates.braveApiKey = body['braveApiKey'] || undefined;
        if (body['braveApiKey']) process.env['BRAVE_SEARCH_API_KEY'] = body['braveApiKey'] as string;
        else delete process.env['BRAVE_SEARCH_API_KEY'];
      }
      if (typeof body['bochaApiKey'] === 'string') {
        updates.bochaApiKey = body['bochaApiKey'] || undefined;
        if (body['bochaApiKey']) process.env['BOCHA_API_KEY'] = body['bochaApiKey'] as string;
        else delete process.env['BOCHA_API_KEY'];
      }
      try {
        saveConfig({ integrations: { search: updates } } as any, this.markusConfigPath);
      } catch (e) {
        log.warn('Failed to persist search settings', { error: String(e) });
      }
      this.auditService?.record({
        orgId: 'system',
        type: 'settings_changed',
        action: 'search',
        detail: 'Search API key settings updated',
        userId: auth.userId,
        success: true,
        metadata: { keys: Object.keys(updates) },
      });
      const { loadConfig: loadCfg } = await import('@markus/shared');
      const currentConfig = loadCfg(this.markusConfigPath);
      const search = currentConfig.integrations?.search ?? {};
      const mask = (key?: string) => key ? '***' + key.slice(-4) : '';
      this.json(res, 200, {
        serper: { configured: !!search.serperApiKey || !!process.env['SERPER_API_KEY'], preview: mask(search.serperApiKey || process.env['SERPER_API_KEY']) },
        brave: { configured: !!search.braveApiKey || !!process.env['BRAVE_SEARCH_API_KEY'], preview: mask(search.braveApiKey || process.env['BRAVE_SEARCH_API_KEY']) },
        bocha: { configured: !!search.bochaApiKey || !!process.env['BOCHA_API_KEY'], preview: mask(search.bochaApiKey || process.env['BOCHA_API_KEY']) },
      });
      return;
    }

    if (path === '/api/settings/llm/models' && req.method === 'GET') {
      if (!this.llmRouter) {
        this.json(res, 200, { models: [] });
        return;
      }
      this.json(res, 200, { models: this.llmRouter.getModelCatalog() });
      return;
    }

    if (path.match(/^\/api\/settings\/llm\/providers\/[^/]+$/) && req.method === 'PATCH') {
      const auth = await this.requireAuth(req, res);
      if (!auth) return;
      if (!this.llmRouter) {
        this.json(res, 503, { error: 'LLM router not available' });
        return;
      }
      const providerName = path.split('/')[5]!;
      const body = await this.readBody(req);
      try {
        this.llmRouter.updateProviderModelConfig(
          providerName,
          body as {
            contextWindow?: number;
            maxOutputTokens?: number;
            cost?: { input: number; output: number; cacheRead?: number; cacheWrite?: number };
          }
        );
        this.auditService?.record({
          orgId: 'system',
          type: 'settings_changed',
          action: 'llm_provider_model_config',
          detail: `PATCH provider ${providerName}`,
          userId: auth.userId,
          success: true,
          metadata: { providerName, body },
        });
        this.json(res, 200, this.llmRouter.getEnhancedSettings());
      } catch (err) {
        this.json(res, 400, { error: String(err) });
      }
      return;
    }

    // Settings — Add new provider
    if (path === '/api/settings/llm/providers' && req.method === 'POST') {
      const auth = await this.requireAuth(req, res);
      if (!auth) return;
      if (!this.llmRouter) {
        this.json(res, 503, { error: 'LLM router not available' });
        return;
      }
      const body = await this.readBody(req);
      const { name, apiKey, baseUrl, model, enabled, contextWindow, maxOutputTokens, cost } = body as {
        name?: string; apiKey?: string; baseUrl?: string; model?: string; enabled?: boolean;
        contextWindow?: number; maxOutputTokens?: number; cost?: { input: number; output: number };
      };
      if (!name || typeof name !== 'string') {
        this.json(res, 400, { error: 'name (string) is required' });
        return;
      }
      if (!model || typeof model !== 'string') {
        this.json(res, 400, { error: 'model (string) is required' });
        return;
      }
      if (name !== 'ollama' && (!apiKey || typeof apiKey !== 'string')) {
        this.json(res, 400, { error: 'apiKey (string) is required' });
        return;
      }
      try {
        this.llmRouter.registerProviderFromConfig(name, {
          provider: name as any,
          model,
          apiKey,
          baseUrl,
        });
        if (enabled === false) {
          this.llmRouter.setProviderEnabled(name, false);
        }
        if (contextWindow || maxOutputTokens || cost) {
          this.llmRouter.updateProviderModelConfig(name, {
            ...(contextWindow ? { contextWindow } : {}),
            ...(maxOutputTokens ? { maxOutputTokens } : {}),
            ...(cost ? { cost } : {}),
          });
        }
        try {
          const { loadConfig: loadCfg } = await import('@markus/shared');
          const currentConfig = loadCfg(this.markusConfigPath);
          const providers = { ...currentConfig.llm.providers };
          providers[name] = {
            ...providers[name],
            apiKey,
            model,
            ...(baseUrl ? { baseUrl } : {}),
            enabled: enabled !== false,
          };
          saveConfig({ llm: { providers } } as any, this.markusConfigPath);
        } catch (e) {
          log.warn('Failed to persist new provider', { error: String(e) });
        }
        this.auditService?.record({
          orgId: 'system',
          type: 'settings_changed',
          action: 'llm_provider_add',
          detail: `Added LLM provider ${name}`,
          userId: auth.userId,
          success: true,
          metadata: { name, model, enabled: enabled !== false },
        });
        this.json(res, 200, this.llmRouter.getEnhancedSettings());
      } catch (err) {
        this.json(res, 400, { error: String(err) });
      }
      return;
    }

    // Settings — Update existing provider
    if (path.match(/^\/api\/settings\/llm\/providers\/[^/]+$/) && req.method === 'PUT') {
      const auth = await this.requireAuth(req, res);
      if (!auth) return;
      if (!this.llmRouter) {
        this.json(res, 503, { error: 'LLM router not available' });
        return;
      }
      const providerName = path.split('/')[5]!;
      const body = await this.readBody(req);
      const { apiKey, baseUrl, model, enabled, contextWindow, maxOutputTokens, cost } = body as {
        apiKey?: string; baseUrl?: string; model?: string; enabled?: boolean;
        contextWindow?: number; maxOutputTokens?: number; cost?: { input: number; output: number };
      };
      try {
        const provider = this.llmRouter.getProvider(providerName);
        if (provider) {
          const configUpdate: any = { provider: providerName };
          if (model) configUpdate.model = model;
          if (apiKey) configUpdate.apiKey = apiKey;
          if (baseUrl !== undefined) configUpdate.baseUrl = baseUrl;
          provider.configure(configUpdate);
        } else if (model) {
          this.llmRouter.registerProviderFromConfig(providerName, {
            provider: providerName as any,
            model,
            apiKey,
            baseUrl,
          });
        }
        if (typeof enabled === 'boolean') {
          this.llmRouter.setProviderEnabled(providerName, enabled);
        }
        if (contextWindow || maxOutputTokens || cost) {
          this.llmRouter.updateProviderModelConfig(providerName, {
            ...(contextWindow ? { contextWindow } : {}),
            ...(maxOutputTokens ? { maxOutputTokens } : {}),
            ...(cost ? { cost } : {}),
          });
        }
        try {
          const { loadConfig: loadCfg } = await import('@markus/shared');
          const currentConfig = loadCfg(this.markusConfigPath);
          const providers = { ...currentConfig.llm.providers };
          const existing = providers[providerName] ?? {};
          providers[providerName] = {
            ...existing,
            ...(apiKey ? { apiKey } : {}),
            ...(model ? { model } : {}),
            ...(baseUrl !== undefined ? { baseUrl: baseUrl || undefined } : {}),
            ...(typeof enabled === 'boolean' ? { enabled } : {}),
          };
          saveConfig({ llm: { providers } } as any, this.markusConfigPath);
        } catch (e) {
          log.warn('Failed to persist provider update', { error: String(e) });
        }
        this.auditService?.record({
          orgId: 'system',
          type: 'settings_changed',
          action: 'llm_provider_update',
          detail: `Updated LLM provider ${providerName}`,
          userId: auth.userId,
          success: true,
          metadata: { providerName, hasApiKey: !!apiKey, model, enabled },
        });
        this.json(res, 200, this.llmRouter.getEnhancedSettings());
      } catch (err) {
        this.json(res, 400, { error: String(err) });
      }
      return;
    }

    // Settings — Delete provider
    if (path.match(/^\/api\/settings\/llm\/providers\/[^/]+$/) && req.method === 'DELETE') {
      const auth = await this.requireAuth(req, res);
      if (!auth) return;
      if (!this.llmRouter) {
        this.json(res, 503, { error: 'LLM router not available' });
        return;
      }
      const providerName = path.split('/')[5]!;
      try {
        this.llmRouter.unregisterProvider(providerName);
        try {
          const { loadConfig: loadCfg } = await import('@markus/shared');
          const currentConfig = loadCfg(this.markusConfigPath);
          const providers = { ...currentConfig.llm.providers };
          delete providers[providerName];
          const configUpdates: any = { llm: { providers } };
          if (currentConfig.llm.defaultProvider === providerName) {
            const remaining = Object.keys(providers).filter(k => providers[k]?.enabled !== false);
            configUpdates.llm.defaultProvider = remaining[0] ?? 'anthropic';
          }
          saveConfig(configUpdates, this.markusConfigPath);
        } catch (e) {
          log.warn('Failed to persist provider deletion', { error: String(e) });
        }
        this.auditService?.record({
          orgId: 'system',
          type: 'settings_changed',
          action: 'llm_provider_delete',
          detail: `Removed LLM provider ${providerName}`,
          userId: auth.userId,
          success: true,
          metadata: { providerName },
        });
        this.json(res, 200, this.llmRouter.getEnhancedSettings());
      } catch (err) {
        this.json(res, 400, { error: String(err) });
      }
      return;
    }

    // Settings — Add custom model to provider catalog
    if (path.match(/^\/api\/settings\/llm\/providers\/[^/]+\/models$/) && req.method === 'POST') {
      const auth = await this.requireAuth(req, res);
      if (!auth) return;
      if (!this.llmRouter) {
        this.json(res, 503, { error: 'LLM router not available' });
        return;
      }
      const providerName = path.split('/')[5]!;
      const body = await this.readBody(req);
      const { id, name, contextWindow, maxOutputTokens, cost, reasoning, inputTypes } = body as {
        id?: string; name?: string; contextWindow?: number; maxOutputTokens?: number;
        cost?: { input: number; output: number; cacheRead?: number; cacheWrite?: number };
        reasoning?: boolean; inputTypes?: Array<'text' | 'image'>;
      };
      if (!id || !name || !contextWindow || !maxOutputTokens || !cost) {
        this.json(res, 400, { error: 'id, name, contextWindow, maxOutputTokens, and cost are required' });
        return;
      }
      try {
        const modelDef = {
          id, name, provider: providerName, contextWindow, maxOutputTokens, cost,
          ...(reasoning !== null && reasoning !== undefined ? { reasoning } : {}),
          ...(inputTypes ? { inputTypes } : {}),
        };
        this.llmRouter.addCustomModel(providerName, modelDef);
        try {
          const { loadConfig: loadCfg } = await import('@markus/shared');
          const currentConfig = loadCfg(this.markusConfigPath);
          const customModels = { ...(currentConfig.llm.customModels ?? {}) };
          const existing = customModels[providerName] ?? [];
          customModels[providerName] = [...existing.filter(m => m.id !== id), modelDef];
          saveConfig({ llm: { customModels } } as any, this.markusConfigPath);
        } catch (e) {
          log.warn('Failed to persist custom model', { error: String(e) });
        }
        this.auditService?.record({
          orgId: 'system',
          type: 'settings_changed',
          action: 'llm_custom_model_add',
          detail: `Custom model ${id} on provider ${providerName}`,
          userId: auth.userId,
          success: true,
          metadata: { providerName, modelId: id },
        });
        this.json(res, 200, this.llmRouter.getEnhancedSettings());
      } catch (err) {
        this.json(res, 400, { error: String(err) });
      }
      return;
    }

    // Settings — Delete custom model from provider catalog
    if (path.match(/^\/api\/settings\/llm\/providers\/[^/]+\/models\/[^/]+$/) && req.method === 'DELETE') {
      const auth = await this.requireAuth(req, res);
      if (!auth) return;
      if (!this.llmRouter) {
        this.json(res, 503, { error: 'LLM router not available' });
        return;
      }
      const parts = path.split('/');
      const providerName = parts[5]!;
      const modelId = decodeURIComponent(parts[7]!);
      try {
        this.llmRouter.removeCustomModel(providerName, modelId);
        try {
          const { loadConfig: loadCfg } = await import('@markus/shared');
          const currentConfig = loadCfg(this.markusConfigPath);
          const customModels = { ...(currentConfig.llm.customModels ?? {}) };
          if (customModels[providerName]) {
            customModels[providerName] = customModels[providerName].filter(m => m.id !== modelId);
            if (customModels[providerName].length === 0) delete customModels[providerName];
          }
          saveConfig({ llm: { customModels } } as any, this.markusConfigPath);
        } catch (e) {
          log.warn('Failed to persist custom model removal', { error: String(e) });
        }
        this.auditService?.record({
          orgId: 'system',
          type: 'settings_changed',
          action: 'llm_custom_model_delete',
          detail: `Removed custom model ${modelId} from ${providerName}`,
          userId: auth.userId,
          success: true,
          metadata: { providerName, modelId },
        });
        this.json(res, 200, this.llmRouter.getEnhancedSettings());
      } catch (err) {
        this.json(res, 400, { error: String(err) });
      }
      return;
    }

    // Settings — Switch provider model
    if (path.match(/^\/api\/settings\/llm\/providers\/[^/]+\/model$/) && req.method === 'POST') {
      const auth = await this.requireAuth(req, res);
      if (!auth) return;
      if (!this.llmRouter) {
        this.json(res, 503, { error: 'LLM router not available' });
        return;
      }
      const providerName = path.split('/')[5]!;
      const body = await this.readBody(req);
      const { model } = body as { model?: string };
      if (!model || typeof model !== 'string') {
        this.json(res, 400, { error: 'model (string) is required' });
        return;
      }
      try {
        this.llmRouter.setProviderModel(providerName, model);
        try {
          const { loadConfig: loadCfg } = await import('@markus/shared');
          const currentConfig = loadCfg(this.markusConfigPath);
          const providers = { ...currentConfig.llm.providers };
          providers[providerName] = { ...providers[providerName], model };
          saveConfig({ llm: { providers } } as any, this.markusConfigPath);
        } catch (e) {
          log.warn('Failed to persist provider model change', { error: String(e) });
        }
        this.auditService?.record({
          orgId: 'system',
          type: 'settings_changed',
          action: 'llm_provider_model_switch',
          detail: `Provider ${providerName} model → ${model}`,
          userId: auth.userId,
          success: true,
          metadata: { providerName, model },
        });
        this.json(res, 200, this.llmRouter.getEnhancedSettings());
      } catch (err) {
        this.json(res, 400, { error: String(err) });
      }
      return;
    }

    // Settings — Toggle provider enabled/disabled
    if (path.match(/^\/api\/settings\/llm\/providers\/[^/]+\/toggle$/) && req.method === 'POST') {
      const auth = await this.requireAuth(req, res);
      if (!auth) return;
      if (!this.llmRouter) {
        this.json(res, 503, { error: 'LLM router not available' });
        return;
      }
      const providerName = path.split('/')[5]!;
      const body = await this.readBody(req);
      const { enabled } = body as { enabled: boolean };
      if (typeof enabled !== 'boolean') {
        this.json(res, 400, { error: 'enabled (boolean) is required' });
        return;
      }
      try {
        const prevDefault = this.llmRouter.getSettings().defaultProvider;
        this.llmRouter.setProviderEnabled(providerName, enabled);
        const newDefault = this.llmRouter.getSettings().defaultProvider;
        try {
          const { loadConfig: loadCfg } = await import('@markus/shared');
          const currentConfig = loadCfg(this.markusConfigPath);
          const providers = { ...currentConfig.llm.providers };
          if (providers[providerName]) {
            providers[providerName] = { ...providers[providerName], enabled };
          } else {
            providers[providerName] = { enabled };
          }
          const configUpdates: any = { llm: { providers } };
          if (prevDefault !== newDefault) {
            configUpdates.llm.defaultProvider = newDefault;
          }
          saveConfig(configUpdates, this.markusConfigPath);
        } catch (e) {
          log.warn('Failed to persist provider enabled state', { error: String(e) });
        }
        this.auditService?.record({
          orgId: 'system',
          type: 'settings_changed',
          action: 'llm_provider_toggle',
          detail: `Provider ${providerName} enabled=${enabled}`,
          userId: auth.userId,
          success: true,
          metadata: { providerName, enabled },
        });
        this.json(res, 200, this.llmRouter.getEnhancedSettings());
      } catch (err) {
        this.json(res, 400, { error: String(err) });
      }
      return;
    }

    // Settings — Test provider connectivity
    if (path.match(/^\/api\/settings\/llm\/providers\/[^/]+\/test$/) && req.method === 'POST') {
      const auth = await this.requireAuth(req, res);
      if (!auth) return;
      if (!this.llmRouter) {
        this.json(res, 503, { error: 'LLM router not available' });
        return;
      }
      const providerName = path.split('/')[5]!;
      const provider = this.llmRouter.getProvider(providerName);
      if (!provider) {
        this.json(res, 404, { ok: false, error: `Provider "${providerName}" not found or not configured` });
        return;
      }
      try {
        const startMs = Date.now();
        const response = await this.llmRouter.chat({
          messages: [{ role: 'user', content: 'Reply with exactly one word: hello' }],
          maxTokens: 32,
          temperature: 0,
        }, providerName);
        const durationMs = Date.now() - startMs;
        const reply = (response.content ?? '').trim();
        if (!reply) {
          this.json(res, 200, {
            ok: false,
            error: 'Model returned empty response — API key or model may be misconfigured',
            model: provider.model,
            durationMs,
          });
        } else {
          this.json(res, 200, {
            ok: true,
            durationMs,
            model: provider.model,
            reply: reply.slice(0, 100),
            usage: response.usage,
          });
        }
      } catch (err) {
        const raw = String(err);
        // Extract HTTP status code if present (e.g. "OpenAI API error 401: ...")
        const statusMatch = raw.match(/(?:API error|status)\s+(\d{3})/i);
        const errorCode = statusMatch ? Number(statusMatch[1]) : undefined;
        // Extract the most useful portion of the error
        let errorMsg = raw.replace(/^Error:\s*/, '');
        // Try to parse JSON error body from the message
        const jsonStart = errorMsg.indexOf('{');
        if (jsonStart >= 0) {
          try {
            const jsonStr = errorMsg.slice(jsonStart);
            const parsed = JSON.parse(jsonStr) as { error?: { message?: string; type?: string; code?: string } };
            if (parsed.error?.message) {
              errorMsg = `[${parsed.error.type ?? parsed.error.code ?? errorCode ?? 'error'}] ${parsed.error.message}`;
            }
          } catch { /* keep original */ }
        }
        this.json(res, 200, {
          ok: false,
          error: errorMsg.slice(0, 500),
          errorCode,
          model: provider.model,
        });
      }
      return;
    }

    // Settings — Detect model configs from environment variables
    if (path === '/api/settings/env-models' && req.method === 'GET') {
      const auth = await this.requireAuth(req, res);
      if (!auth) return;

      const ENV_MODEL_MAP: Array<{
        provider: string;
        displayName: string;
        keyEnv: string;
        modelEnv?: string;
        baseUrlEnv?: string;
        defaultModel: string;
        defaultBaseUrl?: string;
      }> = [
        { provider: 'anthropic', displayName: 'Anthropic', keyEnv: 'ANTHROPIC_API_KEY', defaultModel: 'claude-opus-4-6' },
        { provider: 'openai', displayName: 'OpenAI', keyEnv: 'OPENAI_API_KEY', defaultModel: 'gpt-5.4' },
        { provider: 'google', displayName: 'Google Gemini', keyEnv: 'GOOGLE_API_KEY', defaultModel: 'gemini-3-1-pro' },
        { provider: 'siliconflow', displayName: 'SiliconFlow', keyEnv: 'SILICONFLOW_API_KEY', modelEnv: 'SILICONFLOW_MODEL', baseUrlEnv: 'SILICONFLOW_BASE_URL', defaultModel: 'Qwen/Qwen3.5-35B-A3B', defaultBaseUrl: 'https://api.siliconflow.cn/v1' },
        { provider: 'minimax', displayName: 'MiniMax', keyEnv: 'MINIMAX_API_KEY', modelEnv: 'MINIMAX_MODEL', baseUrlEnv: 'MINIMAX_BASE_URL', defaultModel: 'MiniMax-M2.7', defaultBaseUrl: 'https://api.minimax.io/v1' },
        { provider: 'openrouter', displayName: 'OpenRouter', keyEnv: 'OPENROUTER_API_KEY', modelEnv: 'OPENROUTER_MODEL', baseUrlEnv: 'OPENROUTER_BASE_URL', defaultModel: 'xiaomi/mimo-v2-pro', defaultBaseUrl: 'https://openrouter.ai/api/v1' },
        { provider: 'zai', displayName: 'ZAI', keyEnv: 'ZAI_API_KEY', modelEnv: 'ZAI_MODEL', baseUrlEnv: 'ZAI_BASE_URL', defaultModel: 'glm-5.1', defaultBaseUrl: 'https://api.z.ai/api/paas/v4' },
        { provider: 'deepseek', displayName: 'DeepSeek', keyEnv: 'DEEPSEEK_API_KEY', modelEnv: 'DEEPSEEK_MODEL', baseUrlEnv: 'DEEPSEEK_BASE_URL', defaultModel: 'deepseek-v4-flash', defaultBaseUrl: 'https://api.deepseek.com' },
      ];

      const detected: Array<{
        provider: string;
        displayName: string;
        apiKeySet: boolean;
        apiKeyPreview: string;
        model: string;
        baseUrl?: string;
        envVars: Record<string, string>;
        ollamaModels?: Array<{ name: string; size?: number; parameterSize?: string; family?: string }>;
      }> = [];

      for (const def of ENV_MODEL_MAP) {
        const apiKey = process.env[def.keyEnv];
        if (!apiKey) continue;

        const model = def.modelEnv ? (process.env[def.modelEnv] ?? def.defaultModel) : def.defaultModel;
        const baseUrl = def.baseUrlEnv ? (process.env[def.baseUrlEnv] ?? def.defaultBaseUrl) : def.defaultBaseUrl;
        const envVars: Record<string, string> = { [def.keyEnv]: '***' + apiKey.slice(-4) };
        if (def.modelEnv && process.env[def.modelEnv]) envVars[def.modelEnv] = process.env[def.modelEnv]!;
        if (def.baseUrlEnv && process.env[def.baseUrlEnv]) envVars[def.baseUrlEnv] = process.env[def.baseUrlEnv]!;

        detected.push({
          provider: def.provider,
          displayName: def.displayName,
          apiKeySet: true,
          apiKeyPreview: '***' + apiKey.slice(-4),
          model,
          baseUrl,
          envVars,
        });
      }

      // Also probe local Ollama service (no API key needed)
      try {
        const ollamaUrl = process.env['OLLAMA_BASE_URL'] || 'http://localhost:11434';
        const ctrl = new AbortController();
        const tmr = setTimeout(() => ctrl.abort(), 3000);
        const ollamaRes = await fetch(`${ollamaUrl.replace(/\/+$/, '')}/api/tags`, { signal: ctrl.signal });
        clearTimeout(tmr);
        if (ollamaRes.ok) {
          const ollamaData = await ollamaRes.json() as { models?: Array<{ name: string; size?: number; details?: { parameter_size?: string; family?: string } }> };
          const models = ollamaData.models ?? [];
          if (models.length > 0) {
            detected.push({
              provider: 'ollama',
              displayName: 'Ollama (Local)',
              apiKeySet: false,
              apiKeyPreview: 'local',
              model: models[0]!.name.replace(/:latest$/, ''),
              baseUrl: ollamaUrl,
              envVars: { OLLAMA_BASE_URL: ollamaUrl },
              ollamaModels: models.map(m => ({
                name: m.name.replace(/:latest$/, ''),
                size: m.size,
                parameterSize: m.details?.parameter_size,
                family: m.details?.family,
              })),
            });
          }
        }
      } catch { /* Ollama not running — skip silently */ }

      const timeoutMs = process.env['LLM_TIMEOUT_MS'];
      this.json(res, 200, {
        detected,
        timeoutMs: timeoutMs ? Number(timeoutMs) : undefined,
      });
      return;
    }

    // Settings — Detect local Ollama service and list available models
    if (path === '/api/settings/detect-ollama' && req.method === 'GET') {
      const auth = await this.requireAuth(req, res);
      if (!auth) return;

      const ollamaUrl = process.env['OLLAMA_BASE_URL'] || 'http://localhost:11434';
      try {
        const ctrl = new AbortController();
        const tmr = setTimeout(() => ctrl.abort(), 5000);
        const ollamaRes = await fetch(`${ollamaUrl.replace(/\/+$/, '')}/api/tags`, { signal: ctrl.signal });
        clearTimeout(tmr);
        if (!ollamaRes.ok) {
          this.json(res, 200, { found: false, error: `Ollama returned HTTP ${ollamaRes.status}` });
          return;
        }
        const data = await ollamaRes.json() as { models?: Array<{ name: string; size?: number; modified_at?: string; details?: { parameter_size?: string; family?: string; quantization_level?: string } }> };
        const models = (data.models ?? []).map(m => ({
          name: m.name.replace(/:latest$/, ''),
          fullName: m.name,
          size: m.size,
          modifiedAt: m.modified_at,
          parameterSize: m.details?.parameter_size,
          family: m.details?.family,
          quantization: m.details?.quantization_level,
        }));
        this.json(res, 200, { found: true, baseUrl: ollamaUrl, models });
      } catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        this.json(res, 200, { found: false, error: msg.includes('abort') ? 'Connection timed out' : msg });
      }
      return;
    }

    // Settings — Apply env model configs to markus.json
    if (path === '/api/settings/env-models' && req.method === 'POST') {
      const auth = await this.requireAuth(req, res);
      if (!auth) return;
      const body = await this.readBody(req);
      const { providers: providerUpdates } = body as {
        providers: Array<{
          provider: string;
          model: string;
          baseUrl?: string;
          enabled?: boolean;
        }>;
      };
      if (!Array.isArray(providerUpdates) || providerUpdates.length === 0) {
        this.json(res, 400, { error: 'providers array is required' });
        return;
      }
      try {
        const { loadConfig: loadCfg } = await import('@markus/shared');
        const currentConfig = loadCfg(this.markusConfigPath);
        const updatedProviders = { ...currentConfig.llm.providers };
        const applied: string[] = [];
        for (const pu of providerUpdates) {
          const envKeyMap: Record<string, string> = {
            anthropic: 'ANTHROPIC_API_KEY',
            openai: 'OPENAI_API_KEY',
            google: 'GOOGLE_API_KEY',
            siliconflow: 'SILICONFLOW_API_KEY',
            minimax: 'MINIMAX_API_KEY',
            openrouter: 'OPENROUTER_API_KEY',
            zai: 'ZAI_API_KEY',
            deepseek: 'DEEPSEEK_API_KEY',
          };
          const apiKey = process.env[envKeyMap[pu.provider] ?? ''];
          if (!apiKey && pu.provider !== 'ollama') continue;
          updatedProviders[pu.provider] = {
            ...updatedProviders[pu.provider],
            ...(apiKey ? { apiKey } : {}),
            model: pu.model,
            ...(pu.baseUrl ? { baseUrl: pu.baseUrl } : {}),
            enabled: pu.enabled !== false,
          };
          applied.push(pu.provider);
        }
        saveConfig({ llm: { providers: updatedProviders } } as any, this.markusConfigPath);
        // Hot-register newly applied providers in the running router
        if (this.llmRouter) {
          for (const provName of applied) {
            if (!this.llmRouter.getProvider(provName)) {
              const cfg = updatedProviders[provName];
              if (cfg?.apiKey || provName === 'ollama') {
                try {
                  this.llmRouter.registerProviderFromConfig(provName, {
                    provider: provName as any,
                    model: cfg.model ?? '',
                    apiKey: cfg.apiKey,
                    baseUrl: cfg.baseUrl,
                  });
                } catch (e) {
                  log.warn(`Failed to hot-register provider ${provName}`, { error: String(e) });
                }
              }
            }
          }
        }
        this.json(res, 200, {
          applied,
          message: `Updated ${applied.length} provider(s) in markus.json`,
          ...(this.llmRouter ? { settings: this.llmRouter.getEnhancedSettings() } : {}),
        });
      } catch (err) {
        this.json(res, 400, { error: String(err) });
      }
      return;
    }

    // ─── OAuth Authentication ───

    // List available OAuth providers
    if (path === '/api/settings/oauth/providers' && req.method === 'GET') {
      if (!this.llmRouter?.oauthManager) {
        this.json(res, 200, { providers: [] });
        return;
      }
      const supportedProviders = this.llmRouter.oauthManager.getSupportedProviders().map(name => ({
        name,
        displayName: name === 'openai-codex' ? 'OpenAI Codex (ChatGPT OAuth)' : name,
        config: this.llmRouter!.oauthManager!.getProviderConfig(name),
      }));
      this.json(res, 200, { providers: supportedProviders });
      return;
    }

    // List auth profiles
    if (path === '/api/settings/oauth/profiles' && req.method === 'GET') {
      const auth = await this.requireAuth(req, res);
      if (!auth) return;
      if (!this.llmRouter?.profileStore) {
        this.json(res, 200, { profiles: [] });
        return;
      }
      const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
      const provider = url.searchParams.get('provider') ?? undefined;
      this.json(res, 200, { profiles: this.llmRouter.profileStore.listProfilesSafe(provider) });
      return;
    }

    // Start OAuth login flow
    if (path === '/api/settings/oauth/login' && req.method === 'POST') {
      const auth = await this.requireAuth(req, res);
      if (!auth) return;
      if (!this.llmRouter?.oauthManager) {
        this.json(res, 503, { error: 'OAuth not initialized' });
        return;
      }
      const body = await this.readBody(req);
      const { provider } = body as { provider?: string };
      if (!provider) {
        this.json(res, 400, { error: 'provider is required' });
        return;
      }
      try {
        const { authorizeUrl, promise } = await this.llmRouter.oauthManager.startLogin(provider);
        // Don't await the promise — it resolves when the user completes the browser flow.
        // Instead, respond with the authorizeUrl and let the frontend poll for status.
        promise.then(profile => {
          log.info(`OAuth login completed for ${provider}`, { profileId: profile.id });
          // Auto-register the new OAuth provider in the router
          if (this.llmRouter && !this.llmRouter.getProvider(provider)) {
            try {
              this.llmRouter.registerOAuthProvider(provider, profile, {
                model: provider === 'openai-codex' ? 'gpt-5.4' : undefined,
              });
            } catch (err) {
              log.warn(`Failed to auto-register OAuth provider after login`, { error: String(err) });
            }
          }
        }).catch(err => {
          log.warn(`OAuth login failed for ${provider}`, { error: String(err) });
        });
        this.json(res, 200, { authorizeUrl, provider });
      } catch (err) {
        this.json(res, 400, { error: String(err) });
      }
      return;
    }

    // Handle manual OAuth callback (paste redirect URL for headless scenarios)
    if (path === '/api/settings/oauth/callback' && req.method === 'POST') {
      const auth = await this.requireAuth(req, res);
      if (!auth) return;
      if (!this.llmRouter?.oauthManager) {
        this.json(res, 503, { error: 'OAuth not initialized' });
        return;
      }
      const body = await this.readBody(req);
      const { callbackUrl } = body as { callbackUrl?: string };
      if (!callbackUrl) {
        this.json(res, 400, { error: 'callbackUrl is required' });
        return;
      }
      try {
        const profile = await this.llmRouter.oauthManager.handleManualCallback(callbackUrl);
        if (this.llmRouter && !this.llmRouter.getProvider(profile.provider)) {
          this.llmRouter.registerOAuthProvider(profile.provider, profile, {
            model: profile.provider === 'openai-codex' ? 'gpt-5.4' : undefined,
          });
        }
        this.json(res, 200, {
          profile: {
            id: profile.id,
            provider: profile.provider,
            authType: profile.authType,
            label: profile.label,
            oauthAccountId: profile.oauth?.accountId,
          },
        });
      } catch (err) {
        this.json(res, 400, { error: String(err) });
      }
      return;
    }

    // Check OAuth login status (polling endpoint)
    if (path === '/api/settings/oauth/status' && req.method === 'GET') {
      const auth = await this.requireAuth(req, res);
      if (!auth) return;
      if (!this.llmRouter?.oauthManager) {
        this.json(res, 200, { pending: false, profiles: [] });
        return;
      }
      const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
      const provider = url.searchParams.get('provider') ?? undefined;
      const pending = this.llmRouter.oauthManager.hasPendingLogin(provider);
      const profiles = this.llmRouter.profileStore?.listProfilesSafe(provider) ?? [];
      this.json(res, 200, { pending, profiles });
      return;
    }

    // Delete auth profile
    if (path.match(/^\/api\/settings\/oauth\/profiles\/[^/]+$/) && req.method === 'DELETE') {
      const auth = await this.requireAuth(req, res);
      if (!auth) return;
      if (!this.llmRouter?.profileStore) {
        this.json(res, 404, { error: 'Profile store not available' });
        return;
      }
      const profileId = decodeURIComponent(path.split('/')[5]!);
      const deleted = this.llmRouter.profileStore.deleteProfile(profileId);
      if (deleted) {
        this.json(res, 200, { deleted: true, profileId });
      } else {
        this.json(res, 404, { error: 'Profile not found' });
      }
      return;
    }

    // Store setup token (e.g. Anthropic)
    if (path === '/api/settings/oauth/setup-token' && req.method === 'POST') {
      const auth = await this.requireAuth(req, res);
      if (!auth) return;
      if (!this.llmRouter?.oauthManager) {
        this.json(res, 503, { error: 'OAuth not initialized' });
        return;
      }
      const body = await this.readBody(req);
      const { provider, token } = body as { provider?: string; token?: string };
      if (!provider || !token) {
        this.json(res, 400, { error: 'provider and token are required' });
        return;
      }
      const profile = this.llmRouter.oauthManager.storeSetupToken(provider, token);
      this.json(res, 200, {
        profile: {
          id: profile.id,
          provider: profile.provider,
          authType: profile.authType,
          label: profile.label,
        },
      });
      return;
    }

    // Settings — Config Export
    if (path === '/api/settings/export' && req.method === 'POST') {
      const auth = await this.requireAuth(req, res);
      if (!auth) return;
      const body = await this.readBody(req);
      const sections = (body.sections as string[]) ?? ['llm', 'teams', 'agents', 'templates'];
      const exportData: Record<string, unknown> = {
        version: '1.0',
        exportedAt: new Date().toISOString(),
        sections: {},
      };
      const sectionData = exportData.sections as Record<string, unknown>;
      if (sections.includes('llm') && this.llmRouter) {
        sectionData.llm = this.llmRouter.getEnhancedSettings();
      }
      if (sections.includes('teams')) {
        const teams = this.orgService.listTeams(auth.orgId);
        sectionData.teams = teams;
      }
      if (sections.includes('agents')) {
        const agents = this.orgService.getAgentManager().listAgents();
        sectionData.agents = agents.map(a => ({
          id: a.id,
          name: a.name,
          role: a.role,
          status: a.status,
        }));
      }
      this.json(res, 200, exportData);
      return;
    }

    // Settings — Config Import (preview and apply)
    if (path === '/api/settings/import' && req.method === 'POST') {
      const auth = await this.requireAuth(req, res);
      if (!auth) return;
      const body = await this.readBody(req);
      const { data, preview } = body as { data: Record<string, unknown>; preview?: boolean };
      if (!data || !data.sections) {
        this.json(res, 400, { error: 'Invalid import data: missing sections' });
        return;
      }
      const sections = data.sections as Record<string, unknown>;
      const available = Object.keys(sections);
      if (preview) {
        const summary: Record<string, { count: number; items: string[] }> = {};
        if (sections.llm) {
          const llm = sections.llm as Record<string, unknown>;
          const provCount = llm.providers ? Object.keys(llm.providers as object).length : 0;
          summary.llm = {
            count: provCount,
            items: llm.providers ? Object.keys(llm.providers as object) : [],
          };
        }
        if (sections.teams) {
          const teams = sections.teams as Array<{ name: string }>;
          summary.teams = { count: teams.length, items: teams.map(t => t.name) };
        }
        if (sections.agents) {
          const agents = sections.agents as Array<{ name: string }>;
          summary.agents = { count: agents.length, items: agents.map(a => a.name) };
        }
        this.json(res, 200, { available, summary });
        return;
      }
      // Apply import
      const applied: string[] = [];
      if (sections.llm && this.llmRouter) {
        const llm = sections.llm as {
          defaultProvider?: string;
          providers?: Record<
            string,
            {
              cost?: { input: number; output: number };
              contextWindow?: number;
              maxOutputTokens?: number;
            }
          >;
        };
        if (llm.providers) {
          for (const [name, cfg] of Object.entries(llm.providers)) {
            if (cfg.cost || cfg.contextWindow || cfg.maxOutputTokens) {
              this.llmRouter.updateProviderModelConfig(name, {
                cost: cfg.cost,
                contextWindow: cfg.contextWindow,
                maxOutputTokens: cfg.maxOutputTokens,
              });
            }
          }
        }
        if (llm.defaultProvider && this.llmRouter.listProviders().includes(llm.defaultProvider)) {
          this.llmRouter.setDefaultProvider(llm.defaultProvider);
        }
        applied.push('llm');
      }
      this.json(res, 200, { applied, message: `Imported ${applied.length} sections` });
      return;
    }

    // Settings — Import from OpenClaw config
    if (path === '/api/settings/import/openclaw' && req.method === 'POST') {
      const auth = await this.requireAuth(req, res);
      if (!auth) return;
      const body = await this.readBody(req);
      const { configPath, preview } = body as { configPath?: string; preview?: boolean };

      const { existsSync: fsExists, readFileSync: fsRead } = await import('node:fs');
      const { join: pathJoin } = await import('node:path');
      const { homedir } = await import('node:os');

      const possiblePaths = [
        configPath,
        pathJoin(homedir(), '.openclaw', 'openclaw.json'),
        pathJoin(homedir(), '.openclaw', 'openclaw.json5'),
      ].filter(Boolean) as string[];

      let found = '';
      let rawContent = '';
      for (const p of possiblePaths) {
        if (fsExists(p)) {
          found = p;
          rawContent = fsRead(p, 'utf-8');
          break;
        }
      }

      if (!found) {
        this.json(res, 404, { error: 'No OpenClaw config found', searchedPaths: possiblePaths });
        return;
      }

      try {
        const cleaned = rawContent
          .replace(/\/\/.*$/gm, '')
          .replace(/\/\*[\s\S]*?\*\//g, '')
          .replace(/,\s*([\]}])/g, '$1');
        const parsed = JSON.parse(cleaned) as Record<string, unknown>;

        const modelsSection = parsed.models as
          | {
              providers?: Record<
                string,
                {
                  baseUrl?: string;
                  models?: Array<{
                    id: string;
                    name: string;
                    cost?: { input: number; output: number };
                    contextWindow?: number;
                    maxTokens?: number;
                  }>;
                }
              >;
            }
          | undefined;
        const channelsSection = parsed.channels as Record<string, unknown> | undefined;

        if (preview) {
          const summary: Record<string, unknown> = { configPath: found };
          if (modelsSection?.providers) {
            const provs = Object.entries(modelsSection.providers).map(([name, cfg]) => ({
              name,
              modelCount: cfg.models?.length ?? 0,
              baseUrl: cfg.baseUrl,
            }));
            summary.models = { providerCount: provs.length, providers: provs };
          }
          if (channelsSection) {
            summary.channels = Object.keys(channelsSection).filter(
              k => k !== 'defaults' && k !== 'modelByChannel'
            );
          }
          this.json(res, 200, { found: true, summary });
          return;
        }

        // Apply model configs
        let appliedModels = 0;
        if (modelsSection?.providers && this.llmRouter) {
          for (const [name, cfg] of Object.entries(modelsSection.providers)) {
            if (cfg.models) {
              for (const m of cfg.models) {
                if (m.cost || m.contextWindow || m.maxTokens) {
                  this.llmRouter.updateProviderModelConfig(name, {
                    cost: m.cost,
                    contextWindow: m.contextWindow,
                    maxOutputTokens: m.maxTokens,
                  });
                  appliedModels++;
                }
              }
            }
          }
        }
        this.json(res, 200, { applied: true, appliedModels, configPath: found });
      } catch (err) {
        this.json(res, 400, { error: `Failed to parse OpenClaw config: ${String(err)}` });
      }
      return;
    }

    // ── Workflow Engine ────────────────────────────────────────────────────
    if (path === '/api/workflows' && req.method === 'GET') {
      if (!this.workflowEngine) {
        this.json(res, 200, { executions: [] });
        return;
      }
      const executions = this.workflowEngine.listExecutions().map(e => ({
        id: e.id,
        workflowId: e.workflowId,
        status: e.status,
        startedAt: e.startedAt,
        completedAt: e.completedAt,
        error: e.error,
        stepCount: e.steps.size,
      }));
      this.json(res, 200, { executions });
      return;
    }

    if (path === '/api/workflows' && req.method === 'POST') {
      if (!this.workflowEngine) this.initWorkflowEngine();
      const body = await this.readBody(req);
      const action = body['action'] as string;
      if (action === 'validate') {
        const errors = this.workflowEngine!.validate(body['workflow'] as WorkflowDefinition);
        this.json(res, 200, { valid: errors.length === 0, errors });
        return;
      }
      try {
        const execution = await this.workflowEngine!.start(
          body['workflow'] as WorkflowDefinition,
          (body['inputs'] as Record<string, unknown>) ?? {}
        );
        this.json(res, 201, {
          executionId: execution.id,
          status: execution.status,
          outputs: execution.outputs,
          error: execution.error,
        });
      } catch (err) {
        this.json(res, 400, { error: String(err) });
      }
      return;
    }

    if (path.startsWith('/api/workflows/') && req.method === 'GET') {
      if (!this.workflowEngine) {
        this.json(res, 404, { error: 'No workflow engine' });
        return;
      }
      const executionId = path.split('/')[3]!;
      const execution = this.workflowEngine.getExecution(executionId);
      if (!execution) {
        this.json(res, 404, { error: 'Execution not found' });
        return;
      }
      const steps = [...execution.steps.entries()].map(([id, s]) => ({
        id,
        status: s.status,
        agentId: s.agentId,
        startedAt: s.startedAt,
        completedAt: s.completedAt,
        error: s.error,
        retryCount: s.retryCount,
        output: s.output,
      }));
      this.json(res, 200, { execution: { ...execution, steps } });
      return;
    }

    if (path.startsWith('/api/workflows/') && req.method === 'DELETE') {
      if (!this.workflowEngine) {
        this.json(res, 404, { error: 'No workflow engine' });
        return;
      }
      const executionId = path.split('/')[3]!;
      const cancelled = this.workflowEngine.cancel(executionId);
      this.json(res, 200, { cancelled });
      return;
    }

    // ── Team Templates ───────────────────────────────────────────────────
    if (path === '/api/team-templates' && req.method === 'GET') {
      const query = url.searchParams.get('q');
      const templates = query
        ? this.teamTemplateRegistry.search(query)
        : this.teamTemplateRegistry.list();
      this.json(res, 200, { templates });
      return;
    }

    if (path === '/api/team-templates' && req.method === 'POST') {
      const body = await this.readBody(req);
      const tpl = body as unknown as {
        id: string;
        name: string;
        description: string;
        version: string;
        author: string;
        members: Array<{
          templateId: string;
          name?: string;
          count?: number;
          role?: 'manager' | 'worker';
        }>;
        tags?: string[];
        category?: string;
      };
      if (!tpl.name || !tpl.members?.length) {
        this.json(res, 400, { error: 'name and members are required' });
        return;
      }
      tpl.id = tpl.id || generateId('team');
      tpl.version = tpl.version || '1.0.0';
      tpl.author = tpl.author || 'user';
      this.teamTemplateRegistry.register(tpl);
      this.json(res, 201, { template: tpl });
      return;
    }

    if (path.match(/^\/api\/team-templates\/[^/]+\/files$/) && req.method === 'GET') {
      const id = path.split('/')[3]!;
      const tpl = this.teamTemplateRegistry.get(id);
      if (!tpl) {
        this.json(res, 404, { error: 'Team template not found' });
        return;
      }
      const { existsSync: ex, readFileSync: rf, readdirSync: rd } = await import('node:fs');
      const rolesDir = resolve(process.cwd(), 'templates', 'roles');
      const rolesCandidates = [rolesDir];
      try {
        const thisFile = (await import('node:url')).fileURLToPath(import.meta.url);
        const thisDir = (await import('node:path')).dirname(thisFile);
        rolesCandidates.unshift(resolve(thisDir, '..', 'templates', 'roles'));
        rolesCandidates.push(resolve(thisDir, '..', '..', '..', '..', 'templates', 'roles'));
      } catch { /* skip */ }
      const rolesRoot = rolesCandidates.find(d => ex(d)) ?? rolesDir;
      const files: Record<string, string> = {};
      for (const [idx, member] of tpl.members.entries()) {
        const roleName = member.roleName;
        if (!roleName) continue;
        const memberSlug = kebab(member.name ?? roleName, 'member-' + idx);
        const roleDir = resolve(rolesRoot, roleName);
        if (!ex(roleDir)) continue;
        try {
          for (const entry of rd(roleDir, { withFileTypes: true })) {
            if (entry.isFile() && !entry.name.endsWith('.json')) {
              try { files[`members/${memberSlug}/${entry.name}`] = rf(resolve(roleDir, entry.name), 'utf-8'); } catch { /* skip */ }
            }
          }
        } catch { /* skip */ }
      }
      this.json(res, 200, { files });
      return;
    }

    if (path.startsWith('/api/team-templates/') && req.method === 'GET') {
      const id = path.split('/')[3]!;
      const tpl = this.teamTemplateRegistry.get(id);
      if (!tpl) {
        this.json(res, 404, { error: 'Team template not found' });
        return;
      }
      this.json(res, 200, { template: tpl });
      return;
    }

    if (path.startsWith('/api/team-templates/') && req.method === 'DELETE') {
      const id = path.split('/')[3]!;
      this.teamTemplateRegistry.unregister(id);
      this.json(res, 200, { deleted: true });
      return;
    }

    // System: open a directory in the native file manager
    if (path === '/api/system/open-path' && req.method === 'POST') {
      try {
        const body = await this.readBody(req);
        const dirPath = body['path'] as string;
        if (!dirPath || !existsSync(dirPath)) {
          this.json(res, 400, { error: 'Invalid or non-existent path' });
          return;
        }
        const platform = process.platform;
        if (platform === 'darwin') execSync(`open ${JSON.stringify(dirPath)}`);
        else if (platform === 'win32') execSync(`explorer ${JSON.stringify(dirPath)}`);
        else execSync(`xdg-open ${JSON.stringify(dirPath)}`);
        this.json(res, 200, { ok: true });
      } catch {
        this.json(res, 500, { error: 'Failed to open path' });
      }
      return;
    }

    // Health
    if (path === '/api/health') {
      const health: Record<string, unknown> = {
        status: 'ok',
        version: APP_VERSION,
        agents: this.orgService.getAgentManager().listAgents().length,
      };
      // Non-blocking update check — cached, so normally instant after first call
      try {
        const update = await checkForUpdate();
        if (update.updateAvailable) {
          health.latestVersion = update.latestVersion;
          health.updateAvailable = true;
        }
      } catch { /* never fail health check for this */ }
      this.json(res, 200, health);
      return;
    }

    // ── Governance: System Controls ──────────────────────────────────────────

    if (path === '/api/system/pause-all' && req.method === 'POST') {
      const body = await this.readBody(req);
      const authUser = await this.getAuthUser(req);
      const am = this.orgService.getAgentManager();
      await am.pauseAllAgents(body['reason'] as string | undefined);
      this.auditService?.record({
        orgId: 'system',
        type: 'system_pause_all',
        action: 'pause_all',
        detail: body['reason'] as string,
        userId: authUser?.userId,
        success: true,
      });
      this.json(res, 200, { status: 'paused', message: 'All agents paused' });
      return;
    }

    if (path === '/api/system/resume-all' && req.method === 'POST') {
      const authUser = await this.getAuthUser(req);
      const am = this.orgService.getAgentManager();
      await am.resumeAllAgents();
      this.auditService?.record({
        orgId: 'system',
        type: 'system_resume_all',
        action: 'resume_all',
        userId: authUser?.userId,
        success: true,
      });
      this.json(res, 200, { status: 'resumed', message: 'All agents resumed' });
      return;
    }

    if (path === '/api/system/emergency-stop' && req.method === 'POST') {
      const authUser = await this.getAuthUser(req);
      const am = this.orgService.getAgentManager();
      await am.emergencyStop();
      this.auditService?.record({
        orgId: 'system',
        type: 'system_emergency_stop',
        action: 'emergency_stop',
        userId: authUser?.userId,
        success: true,
      });
      this.json(res, 200, { status: 'stopped', message: 'EMERGENCY STOP — all agents terminated' });
      return;
    }

    if (path === '/api/system/status' && req.method === 'GET') {
      const am = this.orgService.getAgentManager();
      this.json(res, 200, {
        globalPaused: am.isGlobalPaused(),
        emergencyMode: am.isEmergencyMode(),
      });
      return;
    }

    if (path === '/api/system/storage' && req.method === 'GET') {
      try {
        const dataDir = join(homedir(), '.markus');
        const result = this.collectStorageInfo(dataDir);
        this.json(res, 200, result);
      } catch (err) {
        this.json(res, 500, { error: `Storage scan failed: ${String(err)}` });
      }
      return;
    }

    if (path === '/api/system/storage/orphans' && req.method === 'GET') {
      try {
        const result = this.detectOrphans();
        this.json(res, 200, result);
      } catch (err) {
        this.json(res, 500, { error: `Orphan detection failed: ${String(err)}` });
      }
      return;
    }

    if (path === '/api/system/storage/orphans' && req.method === 'DELETE') {
      try {
        const body = await this.readBody(req);
        const ids = Array.isArray(body?.ids) ? body.ids as string[] : undefined;
        const result = this.purgeOrphans(ids);
        this.json(res, 200, result);
      } catch (err) {
        this.json(res, 500, { error: `Orphan cleanup failed: ${String(err)}` });
      }
      return;
    }

    // ── Governance: Announcements ─────────────────────────────────────────

    if (path === '/api/system/announcements' && req.method === 'POST') {
      const body = await this.readBody(req);
      const authUser = await this.getAuthUser(req);
      const am = this.orgService.getAgentManager();
      const announcement = {
        id: generateId('ann'),
        type: (body['type'] as string) ?? 'info',
        title: body['title'] as string,
        content: body['content'] as string,
        priority: (body['priority'] as string) ?? 'normal',
        createdBy: (body['createdBy'] as string) ?? 'human',
        createdAt: new Date().toISOString(),
        expiresAt: body['expiresAt'] as string | undefined,
        targetScope: (body['targetScope'] as string) ?? 'all',
        targetIds: body['targetIds'] as string[] | undefined,
        acknowledged: [],
      };
      am.broadcastAnnouncement(announcement as any);
      this.auditService?.record({
        orgId: 'system',
        type: 'announcement_broadcast',
        action: 'broadcast',
        detail: announcement.title,
        userId: authUser?.userId,
        success: true,
      });
      this.json(res, 201, { announcement });
      return;
    }

    if (path === '/api/system/announcements' && req.method === 'GET') {
      const am = this.orgService.getAgentManager();
      this.json(res, 200, { announcements: am.getActiveAnnouncements() });
      return;
    }

    // ── File existence batch check ──────────────────────────────────────────

    if (path === '/api/files/check' && req.method === 'POST') {
      const body = await this.readBody(req);
      const paths = body?.paths as string[] | undefined;
      if (!Array.isArray(paths) || paths.length === 0) {
        this.json(res, 400, { error: 'Missing "paths" array in request body' });
        return;
      }

      try {
        const { resolve, extname } = await import('node:path');
        const { existsSync, statSync } = await import('node:fs');
        const { homedir } = await import('node:os');
        const home = homedir();

        const results: Record<string, { exists: boolean; isFile: boolean; type: string }> = {};
        const mdExts = ['.md', '.markdown'];
        const htmlExts = ['.html', '.htm'];
        const jsonExts = ['.json'];
        const imageExts = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'];

        for (const p of paths.slice(0, 50)) {
          try {
            const expanded = p.startsWith('~/') ? resolve(home, p.slice(2)) : p === '~' ? home : p;
            const resolved = resolve(expanded);
            if (!existsSync(resolved)) {
              results[p] = { exists: false, isFile: false, type: 'unknown' };
              continue;
            }
            const stat = statSync(resolved);
            const isFile = stat.isFile();
            const ext = extname(resolved).toLowerCase();
            let type = 'text';
            if (mdExts.includes(ext)) type = 'markdown';
            else if (htmlExts.includes(ext)) type = 'html';
            else if (jsonExts.includes(ext)) type = 'json';
            else if (imageExts.includes(ext)) type = 'image';
            else if (!isFile) type = 'directory';
            results[p] = { exists: true, isFile, type };
          } catch {
            results[p] = { exists: false, isFile: false, type: 'unknown' };
          }
        }

        this.json(res, 200, { results });
      } catch (err) {
        this.json(res, 500, { error: `Failed to check files: ${String(err)}` });
      }
      return;
    }

    // ── File preview ──────────────────────────────────────────────────────

    if (path === '/api/files/preview' && req.method === 'GET') {
      const filePath = url.searchParams.get('path');
      if (!filePath) {
        this.json(res, 400, { error: 'Missing "path" query parameter' });
        return;
      }

      try {
        const { resolve, extname } = await import('node:path');
        const { readFileSync, existsSync, statSync } = await import('node:fs');
        const { homedir } = await import('node:os');
        const home = homedir();
        const expanded = filePath.startsWith('~/') ? resolve(home, filePath.slice(2)) : filePath === '~' ? home : filePath;
        const resolved = resolve(expanded);

        if (!existsSync(resolved)) {
          this.json(res, 404, { error: 'File not found' });
          return;
        }

        const stat = statSync(resolved);
        if (stat.isDirectory()) {
          const { readdirSync } = await import('node:fs');
          const { join, extname: extDir } = await import('node:path');
          const entries = readdirSync(resolved, { withFileTypes: true })
            .filter(e => !e.name.startsWith('.'))
            .map(e => {
              const full = join(resolved, e.name);
              const isDir = e.isDirectory();
              let size: number | undefined;
              try { if (!isDir) size = statSync(full).size; } catch { /* skip */ }
              return { name: e.name, path: full, isDirectory: isDir, size, ext: isDir ? '' : extDir(e.name).toLowerCase() };
            })
            .sort((a, b) => {
              if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1;
              return a.name.localeCompare(b.name);
            });
          this.json(res, 200, { type: 'directory', name: resolved.split('/').pop(), path: resolved, entries });
          return;
        }
        if (!stat.isFile()) {
          this.json(res, 400, { error: 'Path is not a file' });
          return;
        }

        const maxSize = 2 * 1024 * 1024; // 2MB limit
        if (stat.size > maxSize) {
          this.json(res, 413, { error: 'File too large for preview', size: stat.size, maxSize });
          return;
        }

        const ext = extname(resolved).toLowerCase();
        const imageExts = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'];
        if (imageExts.includes(ext)) {
          const data = readFileSync(resolved);
          const mimeMap: Record<string, string> = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml' };
          this.json(res, 200, {
            type: 'image',
            name: resolved.split('/').pop(),
            mimeType: mimeMap[ext] ?? 'application/octet-stream',
            content: data.toString('base64'),
          });
        } else {
          const content = readFileSync(resolved, 'utf-8');
          const mdExts = ['.md', '.markdown'];
          const htmlExts = ['.html', '.htm'];
          const jsonExts = ['.json'];
          const csvExts = ['.csv', '.tsv'];
          let fileType = 'text';
          if (mdExts.includes(ext)) fileType = 'markdown';
          else if (htmlExts.includes(ext)) fileType = 'html';
          else if (jsonExts.includes(ext)) fileType = 'json';
          else if (csvExts.includes(ext)) fileType = 'csv';
          this.json(res, 200, {
            type: fileType,
            name: resolved.split('/').pop(),
            content,
          });
        }
      } catch (err) {
        this.json(res, 500, { error: `Failed to read file: ${String(err)}` });
      }
      return;
    }

    // GET /api/files/image?path=... — serve local image as raw binary (for markdown rendering)
    if (path === '/api/files/image' && req.method === 'GET') {
      const filePath = url.searchParams.get('path');
      if (!filePath) {
        this.json(res, 400, { error: 'Missing "path" query parameter' });
        return;
      }
      try {
        const { resolve, extname } = await import('node:path');
        const { readFileSync, existsSync, statSync } = await import('node:fs');
        const { homedir } = await import('node:os');
        const home = homedir();
        const expanded = filePath.startsWith('~/') ? resolve(home, filePath.slice(2)) : filePath === '~' ? home : filePath;
        const resolved = resolve(expanded);

        if (!existsSync(resolved) || !statSync(resolved).isFile()) {
          this.json(res, 404, { error: 'Image not found' });
          return;
        }
        const maxSize = 10 * 1024 * 1024;
        if (statSync(resolved).size > maxSize) {
          this.json(res, 413, { error: 'Image too large' });
          return;
        }
        const ext = extname(resolved).toLowerCase();
        const mimeMap: Record<string, string> = {
          '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
          '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml',
          '.bmp': 'image/bmp', '.ico': 'image/x-icon',
        };
        const mime = mimeMap[ext];
        if (!mime) {
          this.json(res, 400, { error: 'Not an image file' });
          return;
        }
        const data = readFileSync(resolved);
        res.writeHead(200, {
          'Content-Type': mime,
          'Content-Length': data.length,
          'Cache-Control': 'private, max-age=300',
        });
        res.end(data);
      } catch (err) {
        this.json(res, 500, { error: `Failed to read image: ${String(err)}` });
      }
      return;
    }

    if (path === '/api/files/reveal' && req.method === 'POST') {
      const body = await this.readBody(req);
      const filePath = body?.path as string | undefined;
      if (!filePath) {
        this.json(res, 400, { error: 'Missing "path" in request body' });
        return;
      }

      try {
        const { resolve, dirname } = await import('node:path');
        const { existsSync, statSync } = await import('node:fs');
        const { exec } = await import('node:child_process');
        const { homedir } = await import('node:os');
        const home = homedir();
        const expanded = filePath.startsWith('~/') ? resolve(home, filePath.slice(2)) : filePath === '~' ? home : filePath;
        const resolved = resolve(expanded);

        if (!existsSync(resolved)) {
          this.json(res, 404, { error: 'Path not found' });
          return;
        }

        const isDir = statSync(resolved).isDirectory();
        const platform = process.platform;

        let cmd: string;
        if (platform === 'darwin') {
          cmd = isDir ? `open "${resolved}"` : `open -R "${resolved}"`;
        } else if (platform === 'win32') {
          cmd = isDir ? `explorer "${resolved}"` : `explorer /select,"${resolved}"`;
        } else {
          cmd = `xdg-open "${isDir ? resolved : dirname(resolved)}"`;
        }

        exec(cmd, (err) => {
          if (err) {
            log.warn('Failed to reveal file in system browser', { path: resolved, error: String(err) });
          }
        });

        this.json(res, 200, { ok: true, path: resolved });
      } catch (err) {
        this.json(res, 500, { error: `Failed to reveal file: ${String(err)}` });
      }
      return;
    }

    // ── Requirements ─────────────────────────────────────────────────────

    if (path === '/api/requirements' && req.method === 'GET') {
      const orgId = url.searchParams.get('orgId') ?? 'default';
      const status = url.searchParams.get('status') ?? undefined;
      const source = url.searchParams.get('source') ?? undefined;
      const projectId = url.searchParams.get('projectId') ?? undefined;
      if (!this.requirementService) {
        this.json(res, 200, { requirements: [] });
        return;
      }
      this.json(res, 200, {
        requirements: this.requirementService.listRequirements({
          orgId,
          status: status as any,
          source: source as any,
          projectId,
        }),
      });
      return;
    }

    if (path.match(/^\/api\/requirements\/[^/]+$/) && req.method === 'GET') {
      const reqId = path.split('/')[3]!;
      const requirement = this.requirementService?.getRequirement(reqId);
      if (!requirement) {
        this.json(res, 404, { error: 'Requirement not found' });
        return;
      }
      this.json(res, 200, { requirement });
      return;
    }

    if (path === '/api/requirements' && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const body = await this.readBody(req);
      if (!this.requirementService) {
        this.json(res, 503, { error: 'Requirement service not available' });
        return;
      }
      const title = (body['title'] as string | undefined)?.trim();
      const description = (body['description'] as string | undefined)?.trim();
      const projectId = body['projectId'] as string | undefined;
      if (!title) { this.json(res, 400, { error: 'Title is required' }); return; }
      if (!description) { this.json(res, 400, { error: 'Description is required' }); return; }
      if (!projectId) { this.json(res, 400, { error: 'Project is required' }); return; }
      if (!authUser?.userId) { this.json(res, 400, { error: 'Creator identity is required' }); return; }
      try {
        const requirement = this.requirementService.createRequirement({
          orgId: (body['orgId'] as string) ?? 'default',
          title,
          description,
          priority: body['priority'] as TaskPriority | undefined,
          projectId,
          source: 'user',
          createdBy: authUser.userId,
          tags: body['tags'] as string[] | undefined,
        });
        this.json(res, 201, { requirement });
      } catch (e) {
        this.json(res, 400, { error: String(e).replace('Error: ', '') });
      }
      return;
    }

    if (path.match(/^\/api\/requirements\/[^/]+$/) && req.method === 'PUT') {
      const reqId = path.split('/')[3]!;
      if (!this.requirementService) {
        this.json(res, 503, { error: 'Requirement service not available' });
        return;
      }
      const body = await this.readBody(req);
      try {
        const requirement = this.requirementService.updateRequirement(reqId, {
          title: body['title'] as string | undefined,
          description: body['description'] as string | undefined,
          priority: body['priority'] as TaskPriority | undefined,
          tags: body['tags'] as string[] | undefined,
        });
        this.json(res, 200, { requirement });
      } catch (e) {
        this.json(res, 404, { error: String(e) });
      }
      return;
    }

    if (path.match(/^\/api\/requirements\/[^/]+\/status$/) && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const reqId = path.split('/')[3]!;
      if (!this.requirementService) {
        this.json(res, 503, { error: 'Requirement service not available' });
        return;
      }
      const body = await this.readBody(req);
      try {
        const requirement = this.requirementService.updateRequirementStatus(
          reqId,
          body['status'] as string as RequirementStatus,
          authUser.userId
        );
        this.json(res, 200, { requirement });
      } catch (e) {
        this.json(res, 400, { error: String(e) });
      }
      return;
    }

    if (path.match(/^\/api\/requirements\/[^/]+\/approve$/) && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const reqId = path.split('/')[3]!;
      if (!this.requirementService) {
        this.json(res, 503, { error: 'Requirement service not available' });
        return;
      }
      try {
        const requirement = this.requirementService.approveRequirement(
          reqId,
          authUser.userId
        );
        this.auditService?.record({
          orgId: requirement.orgId,
          type: 'requirement_approved',
          action: 'approve_requirement',
          detail: `Requirement "${requirement.title}" approved`,
          userId: authUser?.userId,
          success: true,
          metadata: { requirementId: reqId, projectId: requirement.projectId },
        });
        this.json(res, 200, { requirement });
      } catch (e) {
        this.json(res, 400, { error: String(e) });
      }
      return;
    }

    if (path.match(/^\/api\/requirements\/[^/]+\/reject$/) && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const reqId = path.split('/')[3]!;
      if (!this.requirementService) {
        this.json(res, 503, { error: 'Requirement service not available' });
        return;
      }
      const body = await this.readBody(req);
      try {
        const requirement = this.requirementService.rejectRequirement(
          reqId,
          authUser.userId,
          (body['reason'] as string) ?? ''
        );
        this.auditService?.record({
          orgId: requirement.orgId,
          type: 'requirement_rejected',
          action: 'reject_requirement',
          detail: `Requirement "${requirement.title}" rejected`,
          userId: authUser?.userId,
          success: true,
          metadata: { requirementId: reqId, reason: (body['reason'] as string) ?? '' },
        });
        this.json(res, 200, { requirement });
      } catch (e) {
        this.json(res, 400, { error: String(e) });
      }
      return;
    }

    if (path.match(/^\/api\/requirements\/[^/]+\/cancel$/) && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const reqId = path.split('/')[3]!;
      if (!this.requirementService) {
        this.json(res, 503, { error: 'Requirement service not available' });
        return;
      }
      try {
        const requirement = this.requirementService.cancelRequirement(reqId);
        this.json(res, 200, { requirement });
      } catch (e) {
        this.json(res, 404, { error: String(e) });
      }
      return;
    }

    if (path.match(/^\/api\/requirements\/[^/]+$/) && req.method === 'DELETE') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const reqId = path.split('/')[3]!;
      if (!this.requirementService) {
        this.json(res, 503, { error: 'Requirement service not available' });
        return;
      }
      try {
        const requirement = this.requirementService.cancelRequirement(reqId);
        this.json(res, 200, { requirement });
      } catch (e) {
        this.json(res, 404, { error: String(e) });
      }
      return;
    }

    // ── Requirement Comments ──────────────────────────────────────────────

    if (path.match(/^\/api\/requirements\/[^/]+\/comments$/) && req.method === 'POST') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const reqId = path.split('/')[3]!;
      try {
        const body = await this.readBody(req);
        const mentions = (body['mentions'] as string[] | undefined) ?? [];
        let resolvedAuthorName = (body['authorName'] as string | undefined);
        if (!resolvedAuthorName && authUser?.userId && this.storage?.userRepo) {
          const userRow = await this.storage.userRepo.findById(authUser.userId);
          resolvedAuthorName = userRow?.name;
        }
        const authorId = (body['authorId'] as string) ?? authUser?.userId ?? 'human';
        const authorName = resolvedAuthorName ?? 'User';
        // Single entry point: DB write + WS broadcast + agent notifications
        const result = await this.taskService.postRequirementComment(
          reqId, authorId, authorName,
          body['content'] as string,
          mentions, undefined,
          { authorType: (body['authorType'] as string) ?? 'human', attachments: body['attachments'] as unknown[] | undefined, replyToId: body['replyTo'] as string | undefined },
        );
        this.json(res, 201, { comment: result.comment });
      } catch (err) {
        this.json(res, 500, { error: String(err) });
      }
      return;
    }

    if (path.match(/^\/api\/requirements\/[^/]+\/comments$/) && req.method === 'GET') {
      const reqId = path.split('/')[3]!;
      if (!this.storage?.requirementCommentRepo) {
        this.json(res, 200, { comments: [] });
        return;
      }
      try {
        const comments = await this.storage.requirementCommentRepo.getByRequirement(reqId);
        this.json(res, 200, { comments });
      } catch (err) {
        this.json(res, 500, { error: String(err) });
      }
      return;
    }

    // Requirement status history — list all status transitions for a requirement
    if (path.match(/^\/api\/requirements\/[^/]+\/history$/) && req.method === 'GET') {
      const reqId = path.split('/')[3]!;
      try {
        const history = this.requirementService?.getRequirementStatusHistory(reqId) ?? [];
        this.json(res, 200, { history });
      } catch (err) {
        this.json(res, 500, { error: String(err) });
      }
      return;
    }

    // ── Governance: Projects ──────────────────────────────────────────────

    if (path === '/api/projects' && req.method === 'GET') {
      const orgId = url.searchParams.get('orgId') ?? undefined;
      this.json(res, 200, { projects: this.projectService?.listProjects(orgId) ?? [] });
      return;
    }

    if (path === '/api/projects' && req.method === 'POST') {
      if (!this.projectService) {
        this.json(res, 503, { error: 'Project service not available' });
        return;
      }
      const body = await this.readBody(req);
      const authUser = await this.getAuthUser(req);
      const project = this.projectService.createProject({
        orgId: (body['orgId'] as string) ?? 'default',
        name: body['name'] as string,
        description: (body['description'] as string) ?? '',
        repositories: body['repositories'] as any,
        teamIds: body['teamIds'] as any,
        governancePolicy: body['governancePolicy'] as any,
        createdBy: authUser?.userId,
      });
      this.auditService?.record({
        orgId: project.orgId,
        type: 'project_created',
        action: 'create_project',
        detail: `Project "${project.name}" created`,
        userId: authUser?.userId,
        success: true,
        metadata: { projectId: project.id },
      });
      this.json(res, 201, { project });
      return;
    }

    if (path.match(/^\/api\/projects\/[^/]+$/) && req.method === 'GET') {
      const projectId = path.split('/')[3]!;
      const project = this.projectService?.getProject(projectId);
      if (!project) {
        this.json(res, 404, { error: 'Project not found' });
        return;
      }
      this.json(res, 200, { project });
      return;
    }

    if (path.match(/^\/api\/projects\/[^/]+$/) && req.method === 'PUT') {
      if (!this.projectService) {
        this.json(res, 503, { error: 'Project service not available' });
        return;
      }
      const projectId = path.split('/')[3]!;
      const body = await this.readBody(req);
      try {
        const project = this.projectService.updateProject(projectId, body as any);
        this.json(res, 200, { project });
      } catch (err) {
        this.json(res, 404, { error: String(err) });
      }
      return;
    }

    if (path.match(/^\/api\/projects\/[^/]+$/) && req.method === 'DELETE') {
      if (!this.projectService) {
        this.json(res, 503, { error: 'Project service not available' });
        return;
      }
      const projectId = path.split('/')[3]!;
      this.projectService.deleteProject(projectId);
      this.json(res, 200, { deleted: true });
      return;
    }

    // ── Governance: Task Review ───────────────────────────────────────────

    if (path.match(/^\/api\/tasks\/[^/]+\/accept$/) && req.method === 'POST') {
      const taskId = path.split('/')[3]!;
      try {
        const authUser = await this.getAuthUser(req);
        const body = await this.readBody(req);
        const reviewerId = (body['reviewerId'] as string | undefined) ?? authUser?.userId ?? 'human';
        const task = this.taskService.acceptTask(taskId, reviewerId);
        this.json(res, 200, { task });
      } catch (err) {
        this.json(res, 400, { error: String(err) });
      }
      return;
    }

    if (path.match(/^\/api\/tasks\/[^/]+\/revision$/) && req.method === 'POST') {
      const taskId = path.split('/')[3]!;
      const authUser = await this.getAuthUser(req);
      const body = await this.readBody(req);
      try {
        const task = await this.taskService.requestRevision(
          taskId,
          (body['reason'] as string) ?? 'Revisions needed',
          authUser?.userId
        );
        this.json(res, 200, { task });
      } catch (err) {
        this.json(res, 400, { error: String(err) });
      }
      return;
    }

    if (path.match(/^\/api\/tasks\/[^/]+\/archive$/) && req.method === 'POST') {
      const taskId = path.split('/')[3]!;
      try {
        const task = this.taskService.archiveTask(taskId);
        this.json(res, 200, { task });
      } catch (err) {
        this.json(res, 400, { error: String(err) });
      }
      return;
    }

    // ── Governance: Schedule Control ──────────────────────────────────────

    if (path.match(/^\/api\/tasks\/[^/]+\/schedule\/pause$/) && req.method === 'POST') {
      const taskId = path.split('/')[3]!;
      try {
        const task = this.taskService.getTask(taskId);
        if (!task) { this.json(res, 404, { error: 'Task not found' }); return; }
        if (task.taskType !== 'scheduled' || !task.scheduleConfig) {
          this.json(res, 400, { error: 'Task is not a scheduled task' }); return;
        }
        await this.taskService.updateScheduleConfig(taskId, { ...task.scheduleConfig, paused: true });
        this.json(res, 200, { task: { ...task, scheduleConfig: { ...task.scheduleConfig, paused: true } } });
      } catch (err) {
        this.json(res, 400, { error: String(err) });
      }
      return;
    }

    if (path.match(/^\/api\/tasks\/[^/]+\/schedule\/resume$/) && req.method === 'POST') {
      const taskId = path.split('/')[3]!;
      try {
        const task = this.taskService.getTask(taskId);
        if (!task) { this.json(res, 404, { error: 'Task not found' }); return; }
        if (task.taskType !== 'scheduled' || !task.scheduleConfig) {
          this.json(res, 400, { error: 'Task is not a scheduled task' }); return;
        }
        if (['archived', 'rejected'].includes(task.status)) {
          this.json(res, 400, { error: `Cannot resume schedule for ${task.status} task` }); return;
        }
        const updated = { ...task.scheduleConfig, paused: false };
        await this.taskService.updateScheduleConfig(taskId, updated);
        this.json(res, 200, { task: { ...task, scheduleConfig: updated } });
      } catch (err) {
        this.json(res, 400, { error: String(err) });
      }
      return;
    }

    if (path.match(/^\/api\/tasks\/[^/]+\/schedule\/run-now$/) && req.method === 'POST') {
      const taskId = path.split('/')[3]!;
      try {
        const task = this.taskService.getTask(taskId);
        if (!task) { this.json(res, 404, { error: 'Task not found' }); return; }
        if (task.taskType !== 'scheduled' || !task.scheduleConfig) {
          this.json(res, 400, { error: 'Task is not a scheduled task' }); return;
        }
        if (task.status === 'in_progress') {
          this.json(res, 400, { error: 'Task is already running' }); return;
        }
        if (task.status === 'review') {
          this.json(res, 400, { error: 'Task is awaiting review. Accept or reject before running again.' }); return;
        }
        if (task.status === 'blocked') {
          this.json(res, 400, { error: 'Task is blocked by dependencies' }); return;
        }
        if (task.status === 'pending') {
          this.json(res, 400, { error: 'Task is awaiting approval' }); return;
        }
        if (['rejected', 'archived'].includes(task.status)) {
          this.json(res, 400, { error: `Cannot run task in ${task.status} status` }); return;
        }
        await this.taskService.advanceScheduleConfig(taskId);
        const resettable = ['completed', 'cancelled', 'failed'];
        if (resettable.includes(task.status)) {
          await this.taskService.resetTaskForRerun(taskId);
        } else {
          await this.taskService.runTask(taskId);
        }
        this.json(res, 202, { status: 'running', taskId });
      } catch (err) {
        this.json(res, 400, { error: String(err) });
      }
      return;
    }

    if (path.match(/^\/api\/tasks\/[^/]+\/schedule$/) && req.method === 'PUT') {
      const authUser = await this.requireAuth(req, res);
      if (!authUser) return;
      const taskId = path.split('/')[3]!;
      try {
        const body = await this.readBody(req);
        const task = await this.taskService.updateScheduleFields(taskId, {
          every: body['every'] as string | undefined,
          cron: body['cron'] as string | undefined,
          maxRuns: body['maxRuns'] !== null && body['maxRuns'] !== undefined ? Number(body['maxRuns']) : undefined,
          timezone: body['timezone'] as string | undefined,
        });
        this.json(res, 200, { task });
      } catch (err) {
        this.json(res, 400, { error: String(err) });
      }
      return;
    }

    // ── Governance: Governance Policy ─────────────────────────────────────

    if (path === '/api/governance/policy' && req.method === 'GET') {
      this.json(res, 200, { policy: this.taskService.getGovernancePolicy() });
      return;
    }

    if (path === '/api/governance/policy' && req.method === 'PUT') {
      const body = await this.readBody(req);
      this.taskService.setGovernancePolicy(body as any);
      this.json(res, 200, { policy: this.taskService.getGovernancePolicy() });
      return;
    }

    // ── Governance: Reports ───────────────────────────────────────────────

    if (path === '/api/reports' && req.method === 'GET') {
      this.json(res, 200, {
        reports:
          this.reportService?.listReports({
            scope: url.searchParams.get('scope') ?? undefined,
            scopeId: url.searchParams.get('scopeId') ?? undefined,
            type: url.searchParams.get('type') ?? undefined,
          }) ?? [],
      });
      return;
    }

    if (path === '/api/reports/generate' && req.method === 'POST') {
      if (!this.reportService) {
        this.json(res, 503, { error: 'Report service not available' });
        return;
      }
      const body = await this.readBody(req);
      const period = (body['period'] as string) ?? (body['type'] as string) ?? 'weekly';
      const scope = (body['scope'] as string) ?? 'org';
      const scopeId = (body['orgId'] as string) ?? (body['scopeId'] as string) ?? 'default';

      const now = new Date();
      let periodStart: Date;
      if (body['periodStart']) {
        periodStart = new Date(body['periodStart'] as string);
      } else {
        switch (period) {
          case 'daily': {
            periodStart = new Date(now);
            periodStart.setHours(0, 0, 0, 0);
            break;
          }
          case 'monthly': {
            periodStart = new Date(now.getFullYear(), now.getMonth(), 1);
            break;
          }
          default: {
            periodStart = new Date(now.getTime() - 7 * 86400000);
            break;
          }
        }
      }
      const periodEnd = body['periodEnd'] ? new Date(body['periodEnd'] as string) : now;

      const report = await this.reportService.generateReport({
        type: period as any,
        scope: scope as any,
        scopeId,
        periodStart,
        periodEnd,
        includePlan: body['includePlan'] as boolean,
      });
      this.json(res, 200, { report });
      return;
    }

    if (path.match(/^\/api\/reports\/[^/]+$/) && req.method === 'GET') {
      const reportId = path.split('/')[3]!;
      const report = this.reportService?.getReport(reportId);
      if (!report) {
        this.json(res, 404, { error: 'Report not found' });
        return;
      }
      this.json(res, 200, { report });
      return;
    }

    if (path.match(/^\/api\/reports\/[^/]+\/plan\/approve$/) && req.method === 'POST') {
      if (!this.reportService) {
        this.json(res, 503, { error: 'Report service not available' });
        return;
      }
      const reportId = path.split('/')[3]!;
      const body = await this.readBody(req);
      try {
        const report = this.reportService.approvePlan(
          reportId,
          (body['userId'] as string) ?? 'human'
        );
        this.json(res, 200, { report });
      } catch (err) {
        this.json(res, 400, { error: String(err) });
      }
      return;
    }

    if (path.match(/^\/api\/reports\/[^/]+\/plan\/reject$/) && req.method === 'POST') {
      if (!this.reportService) {
        this.json(res, 503, { error: 'Report service not available' });
        return;
      }
      const reportId = path.split('/')[3]!;
      const body = await this.readBody(req);
      try {
        const report = this.reportService.rejectPlan(
          reportId,
          (body['userId'] as string) ?? 'human',
          (body['reason'] as string) ?? ''
        );
        this.json(res, 200, { report });
      } catch (err) {
        this.json(res, 400, { error: String(err) });
      }
      return;
    }

    if (path.match(/^\/api\/reports\/[^/]+\/feedback$/) && req.method === 'POST') {
      if (!this.reportService) {
        this.json(res, 503, { error: 'Report service not available' });
        return;
      }
      const reportId = path.split('/')[3]!;
      const body = await this.readBody(req);
      const feedback = this.reportService.addFeedback({
        reportId,
        authorId: (body['authorId'] as string) ?? 'human',
        authorName: (body['authorName'] as string) ?? 'Human Manager',
        type: (body['type'] as any) ?? 'comment',
        content: body['content'] as string,
        priority: body['priority'] as any,
        anchor: body['anchor'] as any,
        disclosure: (body['disclosure'] as any) ?? { scope: 'broadcast' },
        saveToKnowledge: body['saveToKnowledge'] as boolean,
        projectId: body['projectId'] as string,
      });
      this.json(res, 201, { feedback });
      return;
    }

    if (path.match(/^\/api\/reports\/[^/]+\/feedback$/) && req.method === 'GET') {
      const reportId = path.split('/')[3]!;
      this.json(res, 200, { feedback: this.reportService?.getFeedback(reportId) ?? [] });
      return;
    }

    // ── Governance: Knowledge (legacy, redirected to /api/deliverables) ────

    if (path === '/api/knowledge/search' && req.method === 'GET') {
      if (!this.deliverableService) { this.json(res, 200, { results: [] }); return; }
      const query = url.searchParams.get('query') ?? undefined;
      const { results } = this.deliverableService.search({ query });
      this.json(res, 200, { results });
      return;
    }

    if (path === '/api/knowledge' && req.method === 'POST') {
      if (!this.deliverableService) { this.json(res, 503, { error: 'Deliverable service not available' }); return; }
      const body = await this.readBody(req);
      try {
        const d = await this.deliverableService.create({
          type: 'file',
          title: body['title'] as string,
          summary: body['content'] as string,
          tags: body['tags'] as string[],
          agentId: body['source'] as string,
        });
        this.json(res, 201, { entry: d });
      } catch (err) {
        this.json(res, 500, { error: String(err) });
      }
      return;
    }

    if (path.match(/^\/api\/knowledge\/[^/]+\/flag-outdated$/) && req.method === 'POST') {
      if (!this.deliverableService) { this.json(res, 503, { error: 'Deliverable service not available' }); return; }
      const knowledgeId = path.split('/')[3]!;
      await this.deliverableService.flagOutdated(knowledgeId);
      this.json(res, 200, { status: 'flagged' });
      return;
    }

    if (path.match(/^\/api\/knowledge\/[^/]+\/verify$/) && req.method === 'POST') {
      if (!this.deliverableService) { this.json(res, 503, { error: 'Deliverable service not available' }); return; }
      const knowledgeId = path.split('/')[3]!;
      await this.deliverableService.update(knowledgeId, { status: 'verified' });
      this.json(res, 200, { status: 'verified' });
      return;
    }

    if (path.match(/^\/api\/knowledge\/[^/]+$/) && req.method === 'DELETE') {
      if (!this.deliverableService) { this.json(res, 503, { error: 'Deliverable service not available' }); return; }
      const knowledgeId = path.split('/')[3]!;
      await this.deliverableService.remove(knowledgeId);
      this.json(res, 200, { status: 'deleted' });
      return;
    }

    // Serve pre-built Web UI static files as SPA fallback
    if (this.webUiDir) {
      const safePath = path.replace(/\.\./g, '').replace(/\/\//g, '/');
      const filePath = join(this.webUiDir, safePath === '/' ? 'index.html' : safePath);
      if (existsSync(filePath) && statSync(filePath).isFile()) {
        this.serveStaticFile(res, filePath, req);
        return;
      }
      // SPA fallback: serve index.html for non-API routes
      const indexPath = join(this.webUiDir, 'index.html');
      if (existsSync(indexPath) && !path.startsWith('/api/')) {
        this.serveStaticFile(res, indexPath, req);
        return;
      }
    }

    // ── 405 Method Not Allowed check ──────────────────────────────────────
    // If the path matches a known API route pattern but with wrong method
    if (path.startsWith('/api/') && req.method) {
      const routeMatch = this.checkMethodAllowed(path, req.method);
      if (routeMatch) {
        res.setHeader('Allow', routeMatch.join(', '));
        this.json(res, 405, { error: 'Method Not Allowed' });
        return;
      }
    }

    this.json(res, 404, { error: 'Not found' });
  }

  /** Build path → allowed methods table (compiled once) */
  static buildRouteTable(): Array<{ test: (path: string) => boolean; methods: string[] }> {
    // Helper: exact path match
    const exact = (p: string, ...methods: string[]) => ({
      test: (path: string) => path === p,
      methods,
    });
    // Helper: regex path match (from raw regex string)
    const regex = (pattern: RegExp, ...methods: string[]) => ({
      test: (path: string) => pattern.test(path),
      methods,
    });
    // Helper: startsWith path match
    const startsWith = (prefix: string, ...methods: string[]) => ({
      test: (path: string) => path.startsWith(prefix),
      methods,
    });

    return [
      // ── Auth ─────────────────────────────────────────────────────────────
      exact('/api/auth/login', 'POST'),
      exact('/api/auth/logout', 'POST'),
      exact('/api/auth/me', 'GET'),
      exact('/api/auth/change-password', 'POST'),
      exact('/api/auth/setup', 'POST'),
      exact('/api/auth/invite-info', 'GET'),
      exact('/api/auth/profile', 'PUT'),

      // ── Avatars ──────────────────────────────────────────────────────────
      exact('/api/avatars/upload', 'POST'),
      startsWith('/api/avatars/', 'GET'),

      // ── Uploads ───────────────────────────────────────────────────────────
      exact('/api/uploads', 'POST'),
      startsWith('/api/uploads/', 'GET'),

      // ── Agents ───────────────────────────────────────────────────────────
      exact('/api/agents', 'GET', 'POST'),
      exact('/api/agents/role-updates', 'GET'),
      regex(/^\/api\/agents\/[^/]+\/sessions$/, 'GET'),
      regex(/^\/api\/agents\/[^/]+\/(start|stop|daily-report|a2a|message)$/, 'POST'),
      regex(/^\/api\/agents\/[^/]+$/, 'GET', 'DELETE'),
      regex(/^\/api\/agents\/[^/]+\/mind$/, 'GET'),
      regex(/^\/api\/agents\/[^/]+\/mailbox$/, 'GET'),
      regex(/^\/api\/agents\/[^/]+\/decisions$/, 'GET'),
      regex(/^\/api\/agents\/[^/]+\/metrics$/, 'GET'),
      regex(/^\/api\/agents\/[^/]+\/config$/, 'PATCH'),
      regex(/^\/api\/agents\/[^/]+\/memory$/, 'GET'),
      regex(/^\/api\/agents\/[^/]+\/memory\/sessions\/[^/]+$/, 'GET'),
      regex(/^\/api\/agents\/[^/]+\/memory\/daily$/, 'PUT'),
      regex(/^\/api\/agents\/[^/]+\/memory\/longterm$/, 'PUT'),
      regex(/^\/api\/agents\/[^/]+\/files$/, 'GET'),
      regex(/^\/api\/agents\/[^/]+\/files\/[^/]+$/, 'PUT'),
      regex(/^\/api\/agents\/[^/]+\/system-prompt$/, 'PUT'),
      regex(/^\/api\/agents\/[^/]+\/role-status$/, 'GET'),
      regex(/^\/api\/agents\/[^/]+\/role-diff$/, 'GET'),
      regex(/^\/api\/agents\/[^/]+\/role-sync$/, 'POST'),
      regex(/^\/api\/agents\/[^/]+\/role-smart-sync$/, 'POST'),
      regex(/^\/api\/agents\/[^/]+\/skills$/, 'POST'),
      regex(/^\/api\/agents\/[^/]+\/skills\/[^/]+$/, 'DELETE'),
      regex(/^\/api\/agents\/[^/]+\/tools\/[^/]+\/toggle$/, 'POST'),
      regex(/^\/api\/agents\/[^/]+\/activities$/, 'GET'),
      regex(/^\/api\/agents\/[^/]+\/recent-activities$/, 'GET'),
      regex(/^\/api\/agents\/[^/]+\/activity-logs$/, 'GET'),
      regex(/^\/api\/agents\/[^/]+\/heartbeat$/, 'GET'),
      regex(/^\/api\/agents\/[^/]+\/heartbeat\/trigger$/, 'POST'),

      // ── Sessions / Channels ──────────────────────────────────────────────
      regex(/^\/api\/sessions\/[^/]+\/messages$/, 'GET'),
      regex(/^\/api\/sessions\/[^/]+$/, 'DELETE'),
      regex(/^\/api\/channels\/[^/]+\/messages$/, 'GET', 'POST'),

      // ── Group Chats ──────────────────────────────────────────────────────
      exact('/api/group-chats', 'GET', 'POST'),
      regex(/^\/api\/group-chats\/[^/]+$/, 'GET', 'PATCH', 'DELETE'),
      regex(/^\/api\/group-chats\/[^/]+\/members$/, 'POST'),
      regex(/^\/api\/group-chats\/[^/]+\/members\/[^/]+$/, 'DELETE'),

      // ── Teams ────────────────────────────────────────────────────────────
      exact('/api/teams', 'GET', 'POST'),
      regex(/^\/api\/teams\/[^/]+$/, 'PATCH', 'DELETE'),
      regex(/^\/api\/teams\/[^/]+\/members$/, 'POST'),
      regex(/^\/api\/teams\/[^/]+\/members\/[^/]+$/, 'DELETE'),
      regex(/^\/api\/teams\/[^/]+\/(start|stop|pause|resume)$/, 'POST'),
      regex(/^\/api\/teams\/[^/]+\/status$/, 'GET'),
      regex(/^\/api\/teams\/[^/]+\/files$/, 'GET'),
      regex(/^\/api\/teams\/[^/]+\/files\/[^/]+$/, 'GET', 'PUT'),
      regex(/^\/api\/teams\/[^/]+\/export$/, 'GET'),

      // ── Roles ────────────────────────────────────────────────────────────
      exact('/api/roles', 'GET'),
      startsWith('/api/roles/', 'GET'),

      // ── Tasks ───────────────────────────────────────────────────────────
      exact('/api/tasks', 'GET', 'POST'),
      exact('/api/tasks/scheduled', 'GET'),
      exact('/api/tasks/deliverables', 'GET'),
      exact('/api/tasks/dashboard', 'GET'),
      exact('/api/taskboard', 'GET'),
      exact('/api/ops/dashboard', 'GET'),
      regex(/^\/api\/tasks\/[^/]+$/, 'GET'),
      startsWith('/api/tasks/', 'PUT', 'DELETE'),
      regex(/^\/api\/tasks\/[^/]+\/approve$/, 'POST'),
      regex(/^\/api\/tasks\/[^/]+\/reject$/, 'POST'),
      regex(/^\/api\/tasks\/[^/]+\/cancel$/, 'POST'),
      regex(/^\/api\/tasks\/[^/]+\/dependents$/, 'GET'),
      regex(/^\/api\/tasks\/[^/]+\/run$/, 'POST'),
      regex(/^\/api\/tasks\/[^/]+\/subtasks$/, 'GET', 'POST'),
      regex(/^\/api\/tasks\/[^/]+\/subtasks\/[^/]+$/, 'DELETE'),
      regex(/^\/api\/tasks\/[^/]+\/subtasks\/[^/]+\/(complete|cancel)$/, 'POST'),
      regex(/^\/api\/tasks\/[^/]+\/comments$/, 'GET', 'POST'),
      regex(/^\/api\/tasks\/[^/]+\/history$/, 'GET'),
      regex(/^\/api\/tasks\/[^/]+\/pause$/, 'POST'),
      regex(/^\/api\/tasks\/[^/]+\/resume$/, 'POST'),
      regex(/^\/api\/tasks\/[^/]+\/retry$/, 'POST'),
      regex(/^\/api\/tasks\/[^/]+\/revision$/, 'POST'),
      regex(/^\/api\/tasks\/[^/]+\/accept$/, 'POST'),
      regex(/^\/api\/tasks\/[^/]+\/archive$/, 'POST'),
      regex(/^\/api\/tasks\/[^/]+\/schedule$/, 'PUT'),
      regex(/^\/api\/tasks\/[^/]+\/schedule\/pause$/, 'POST'),
      regex(/^\/api\/tasks\/[^/]+\/schedule\/resume$/, 'POST'),
      regex(/^\/api\/tasks\/[^/]+\/schedule\/run-now$/, 'POST'),
      regex(/^\/api\/tasks\/[^/]+\/logs$/, 'GET'),
      regex(/^\/api\/tasks\/[^/]+\/logs\/summary$/, 'GET'),

      // ── Execution ────────────────────────────────────────────────────────
      exact('/api/execution-logs', 'GET'),

      // ── Deliverables / Knowledge ─────────────────────────────────────────
      exact('/api/deliverables', 'GET', 'POST'),
      regex(/^\/api\/deliverables\/[^/]+$/, 'GET', 'PUT', 'DELETE'),
      exact('/api/knowledge', 'POST'),
      exact('/api/knowledge/search', 'GET'),
      regex(/^\/api\/knowledge\/[^/]+$/, 'DELETE'),
      regex(/^\/api\/knowledge\/[^/]+\/flag-outdated$/, 'POST'),
      regex(/^\/api\/knowledge\/[^/]+\/verify$/, 'POST'),

      // ── Reviews ──────────────────────────────────────────────────────────
      exact('/api/reviews', 'GET', 'POST'),
      regex(/^\/api\/reviews\/[^/]+$/, 'GET'),

      // ── Requirements ─────────────────────────────────────────────────────
      exact('/api/requirements', 'GET', 'POST'),
      regex(/^\/api\/requirements\/[^/]+$/, 'GET', 'PUT', 'DELETE'),
      regex(/^\/api\/requirements\/[^/]+\/status$/, 'POST'),
      regex(/^\/api\/requirements\/[^/]+\/approve$/, 'POST'),
      regex(/^\/api\/requirements\/[^/]+\/reject$/, 'POST'),
      regex(/^\/api\/requirements\/[^/]+\/cancel$/, 'POST'),
      regex(/^\/api\/requirements\/[^/]+\/comments$/, 'GET', 'POST'),
      regex(/^\/api\/requirements\/[^/]+\/history$/, 'GET'),

      // ── Projects ─────────────────────────────────────────────────────────
      exact('/api/projects', 'GET', 'POST'),
      regex(/^\/api\/projects\/[^/]+$/, 'GET', 'PUT', 'DELETE'),

      // ── Notifications ────────────────────────────────────────────────────
      exact('/api/notifications', 'GET'),
      exact('/api/notifications/mark-all-read', 'POST'),
      startsWith('/api/notifications/', 'POST'),

      // ── Activity feed ─────────────────────────────────────────────────
      exact('/api/activity', 'GET'),

      // ── Users ────────────────────────────────────────────────────────────
      exact('/api/users', 'GET', 'POST'),
      regex(/^\/api\/users\/[^/]+$/, 'PATCH'),
      regex(/^\/api\/users\/[^/]+\/reset-password$/, 'POST'),
      regex(/^\/api\/users\/[^/]+\/reinvite$/, 'POST'),
      startsWith('/api/users/', 'DELETE'),

      // ── API Keys ─────────────────────────────────────────────────────────
      exact('/api/keys', 'GET', 'POST'),
      startsWith('/api/keys/', 'DELETE'),

      // ── Orgs ─────────────────────────────────────────────────────────────
      exact('/api/orgs', 'GET', 'POST'),

      // ── Message ──────────────────────────────────────────────────────────
      exact('/api/message', 'POST'),

      // ── Skills ──────────────────────────────────────────────────────────
      exact('/api/skills', 'GET'),
      exact('/api/skills/builtin', 'GET'),
      exact('/api/skills/install', 'POST'),
      exact('/api/skills/registry', 'GET'),
      exact('/api/skills/registry/skillhub', 'GET'),
      exact('/api/skills/registry/skillssh', 'GET'),
      regex(/^\/api\/skills\/[^/]+$/, 'GET'),
      regex(/^\/api\/skills\/[^/]+\/files$/, 'GET'),
      startsWith('/api/skills/installed/', 'DELETE'),
      // ── Settings ─────────────────────────────────────────────────────────
      exact('/api/settings/hub', 'GET'),
      exact('/api/settings/hub-token', 'POST'),
      exact('/api/settings/llm', 'GET', 'POST'),
      exact('/api/settings/llm/models', 'GET'),
      exact('/api/settings/agent', 'GET', 'POST'),
      exact('/api/settings/browser', 'GET', 'POST'),
      exact('/api/settings/browser/check', 'GET'),
      exact('/api/settings/browser/test-auto-click', 'POST'),
      exact('/api/settings/browser/test-concurrent', 'POST', 'DELETE'),
      exact('/api/settings/search', 'GET', 'POST'),
      exact('/api/settings/env-models', 'GET', 'POST'),
      exact('/api/settings/detect-ollama', 'GET'),
      exact('/api/settings/export', 'POST'),
      exact('/api/settings/import', 'POST'),
      exact('/api/settings/import/openclaw', 'POST'),
      regex(/^\/api\/settings\/llm\/providers\/[^/]+$/, 'PATCH', 'PUT', 'DELETE'),
      exact('/api/settings/llm/providers', 'POST'),
      regex(/^\/api\/settings\/llm\/providers\/[^/]+\/models$/, 'POST'),
      regex(/^\/api\/settings\/llm\/providers\/[^/]+\/models\/[^/]+$/, 'DELETE'),
      regex(/^\/api\/settings\/llm\/providers\/[^/]+\/model$/, 'POST'),
      regex(/^\/api\/settings\/llm\/providers\/[^/]+\/toggle$/, 'POST'),
      regex(/^\/api\/settings\/llm\/providers\/[^/]+\/test$/, 'POST'),
      exact('/api/settings/oauth/providers', 'GET'),
      exact('/api/settings/oauth/profiles', 'GET'),
      exact('/api/settings/oauth/login', 'POST'),
      exact('/api/settings/oauth/callback', 'POST'),
      exact('/api/settings/oauth/status', 'GET'),
      regex(/^\/api\/settings\/oauth\/profiles\/[^/]+$/, 'DELETE'),
      exact('/api/settings/oauth/setup-token', 'POST'),

      // ── Approvals ────────────────────────────────────────────────────────
      exact('/api/approvals', 'GET', 'POST'),
      startsWith('/api/approvals/', 'POST'),

      // ── Usage ────────────────────────────────────────────────────────────
      exact('/api/usage', 'GET'),
      exact('/api/usage/agents', 'GET'),

      // ── Audit ────────────────────────────────────────────────────────────
      exact('/api/audit', 'GET'),
      exact('/api/audit/summary', 'GET'),
      exact('/api/audit/tokens', 'GET'),

      // ── Plan ─────────────────────────────────────────────────────────────
      exact('/api/plan', 'GET', 'POST'),

      // ── Reports ──────────────────────────────────────────────────────────
      exact('/api/reports', 'GET'),
      exact('/api/reports/generate', 'POST'),
      regex(/^\/api\/reports\/[^/]+$/, 'GET'),
      regex(/^\/api\/reports\/[^/]+\/plan\/approve$/, 'POST'),
      regex(/^\/api\/reports\/[^/]+\/plan\/reject$/, 'POST'),
      regex(/^\/api\/reports\/[^/]+\/feedback$/, 'GET', 'POST'),

      // ── Gateway ──────────────────────────────────────────────────────────
      exact('/api/gateway/info', 'GET'),
      exact('/api/gateway/register', 'POST'),
      exact('/api/gateway/auth', 'POST'),
      exact('/api/gateway/message', 'POST'),
      exact('/api/gateway/status', 'GET'),
      exact('/api/gateway/manual', 'GET'),
      exact('/api/gateway/team', 'GET'),
      exact('/api/gateway/projects', 'GET'),
      exact('/api/gateway/requirements', 'GET'),
      exact('/api/gateway/deliverables', 'GET', 'POST'),
      exact('/api/gateway/sync', 'POST'),
      regex(/^\/api\/gateway\/deliverables\/[^/]+$/, 'PUT'),
      regex(/^\/api\/gateway\/tasks\/([^/]+)\/(accept|progress|complete|fail|delegate|subtasks)$/, 'POST'),

      // ── Builder ──────────────────────────────────────────────────────────
      exact('/api/builder/artifacts', 'GET'),
      exact('/api/builder/artifacts/installed', 'GET'),
      exact('/api/builder/artifacts/save', 'POST'),
      exact('/api/builder/artifacts/import', 'POST'),
      regex(/^\/api\/builder\/artifacts\/(agents?|teams?|skills?)\/([^/]+)$/, 'GET', 'DELETE'),
      regex(/^\/api\/builder\/artifacts\/(agents?|teams?|skills?)\/([^/]+)\/install$/, 'POST'),
      regex(/^\/api\/builder\/artifacts\/(agents?|teams?|skills?)\/([^/]+)\/uninstall$/, 'POST'),
      regex(/^\/api\/builder\/artifacts\/(agents?|teams?|skills?)\/([^/]+)\/images$/, 'POST'),
      regex(/^\/api\/builder\/artifacts\/(agents?|teams?|skills?)\/([^/]+)\/images\/([^/]+)$/, 'GET', 'DELETE'),

      // ── Hub ──────────────────────────────────────────────────────────────
      exact('/api/hub/publish', 'POST'),
      startsWith('/api/hub/', 'GET', 'POST', 'PUT', 'PATCH'),

      // ── Templates ────────────────────────────────────────────────────────
      exact('/api/templates', 'GET'),
      exact('/api/templates/instantiate', 'POST'),
      exact('/api/templates/teams', 'GET'),
      regex(/^\/api\/templates\/[^/]+$/, 'GET'),

      // ── Team Templates ───────────────────────────────────────────────────
      exact('/api/team-templates', 'GET', 'POST'),
      startsWith('/api/team-templates/', 'GET', 'DELETE'),

      // ── Governance / Workflows ───────────────────────────────────────────
      exact('/api/governance/policy', 'GET', 'PUT'),
      exact('/api/workflows', 'GET', 'POST'),
      startsWith('/api/workflows/', 'GET', 'DELETE'),

      // ── System ───────────────────────────────────────────────────────────
      exact('/api/health', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'),
      exact('/api/system/pause-all', 'POST'),
      exact('/api/system/resume-all', 'POST'),
      exact('/api/system/emergency-stop', 'POST'),
      exact('/api/system/status', 'GET'),
      exact('/api/system/storage', 'GET'),
      exact('/api/system/storage/orphans', 'GET', 'DELETE'),
      exact('/api/system/announcements', 'GET', 'POST'),
      exact('/api/system/open-path', 'POST'),

      // ── Files ────────────────────────────────────────────────────────────
      exact('/api/files/check', 'POST'),
      exact('/api/files/preview', 'GET'),
      exact('/api/files/image', 'GET'),
      exact('/api/files/reveal', 'POST'),

      // ── External Agents ──────────────────────────────────────────────────
      exact('/api/external-agents', 'GET'),
      exact('/api/external-agents/register', 'POST'),
      regex(/^\/api\/external-agents\/[^/]+$/, 'DELETE'),
    ];
  }

  /** Check if path matches a known route with wrong method; returns allowed methods if mismatch */
  private checkMethodAllowed(path: string, method: string): string[] | null {
    const table = APIServer.buildRouteTable();
    for (const entry of table) {
      if (entry.test(path)) {
        if (!entry.methods.includes(method)) {
          return entry.methods;
        }
      }
    }
    return null;
  }

  private serveStaticFile(res: ServerResponse, filePath: string, req?: IncomingMessage): void {
    const ext = filePath.split('.').pop()?.toLowerCase() ?? '';
    const MIME: Record<string, string> = {
      html: 'text/html; charset=utf-8',
      js: 'application/javascript; charset=utf-8',
      mjs: 'application/javascript; charset=utf-8',
      css: 'text/css; charset=utf-8',
      json: 'application/json; charset=utf-8',
      png: 'image/png',
      jpg: 'image/jpeg',
      jpeg: 'image/jpeg',
      gif: 'image/gif',
      svg: 'image/svg+xml',
      ico: 'image/x-icon',
      woff: 'font/woff',
      woff2: 'font/woff2',
      ttf: 'font/ttf',
      map: 'application/json',
    };
    const contentType = MIME[ext] ?? 'application/octet-stream';
    const body = readFileSync(filePath);
    const cacheControl = ext === 'html' ? 'no-cache' : 'public, max-age=31536000, immutable';

    const COMPRESSIBLE = new Set(['html', 'js', 'mjs', 'css', 'json', 'svg', 'map']);
    const acceptEncoding = req?.headers?.['accept-encoding'] ?? '';
    if (COMPRESSIBLE.has(ext) && body.byteLength > 1024 && typeof acceptEncoding === 'string' && acceptEncoding.includes('gzip')) {
      const compressed = gzipSync(body);
      res.writeHead(200, {
        'Content-Type': contentType,
        'Content-Length': compressed.byteLength,
        'Content-Encoding': 'gzip',
        'Cache-Control': cacheControl,
        'Vary': 'Accept-Encoding',
      });
      res.end(compressed);
      return;
    }

    res.writeHead(200, {
      'Content-Type': contentType,
      'Content-Length': body.byteLength,
      'Cache-Control': cacheControl,
    });
    res.end(body);
  }

  private projectService?: ProjectService;
  private reportService?: ReportService;
  private knowledgeService?: KnowledgeService;
  private deliverableService?: DeliverableService;
  private requirementService?: RequirementService;

  setProjectService(svc: ProjectService): void {
    this.projectService = svc;
  }
  setReportService(svc: ReportService): void {
    this.reportService = svc;
  }
  setKnowledgeService(svc: KnowledgeService): void {
    this.knowledgeService = svc;
  }
  setDeliverableService(svc: DeliverableService): void {
    this.deliverableService = svc;
  }
  setRequirementService(svc: RequirementService): void {
    this.requirementService = svc;
  }

  private buildOpsDashboard(orgId: string | undefined, period: '1h' | '24h' | '7d') {
    const taskDashboard = this.taskService.getDashboard(orgId);

    // Agent efficiency ranking with health scores
    const agentManager = this.orgService.getAgentManager();
    const allAgents = agentManager.listAgents();
    const agentRanking = allAgents
      .map(a => {
        try {
          const agent = agentManager.getAgent(a.id);
          const metrics = agent.getMetrics(period);
          return {
            agentId: a.id,
            agentName: a.name,
            role: a.role,
            agentRole: a.agentRole,
            status: a.status,
            healthScore: metrics.healthScore,
            tokenUsage: metrics.tokenUsage,
            taskMetrics: metrics.taskMetrics,
            averageResponseTimeMs: metrics.averageResponseTimeMs,
            errorRate: metrics.errorRate,
            totalInteractions: metrics.totalInteractions,
          };
        } catch {
          return {
            agentId: a.id,
            agentName: a.name,
            role: a.role,
            agentRole: a.agentRole,
            status: a.status,
            healthScore: 0,
            tokenUsage: { input: 0, output: 0, cost: 0 },
            taskMetrics: { completed: 0, failed: 0, cancelled: 0, averageCompletionTimeMs: 0 },
            averageResponseTimeMs: 0,
            errorRate: 0,
            totalInteractions: 0,
          };
        }
      })
      .sort((a, b) => b.healthScore - a.healthScore);

    // System health summary
    const healthScores = agentRanking.map(a => a.healthScore);
    const avgHealth =
      healthScores.length > 0
        ? Math.round(healthScores.reduce((s, h) => s + h, 0) / healthScores.length)
        : 0;
    const criticalAgents = agentRanking.filter(a => a.healthScore < 50);
    const totalTokenCost = agentRanking.reduce((s, a) => s + a.tokenUsage.cost, 0);
    const totalInteractions = agentRanking.reduce((s, a) => s + a.totalInteractions, 0);

    const terminalTaskCount =
      taskDashboard.statusCounts.completed +
      taskDashboard.statusCounts.failed +
      taskDashboard.statusCounts.rejected;
    const taskSuccessRate =
      terminalTaskCount > 0
        ? Math.round((taskDashboard.statusCounts.completed / terminalTaskCount) * 100)
        : 0;

    const blockedTasks = taskDashboard.statusCounts.blocked ?? 0;

    return {
      period,
      generatedAt: new Date().toISOString(),
      systemHealth: {
        overallScore: avgHealth,
        activeAgents: allAgents.filter(a => a.status !== 'offline').length,
        totalAgents: allAgents.length,
        criticalAgents: criticalAgents.map(a => ({
          id: a.agentId,
          name: a.agentName,
          score: a.healthScore,
        })),
        totalTokenCost: Math.round(totalTokenCost * 10000) / 10000,
        totalInteractions,
      },
      taskKPI: {
        totalTasks: taskDashboard.totalTasks,
        statusCounts: taskDashboard.statusCounts,
        successRate: taskSuccessRate,
        blockedCount: blockedTasks,
        stuckBlockedCount: taskDashboard.stuckBlockedCount,
        averageCompletionTimeMs: taskDashboard.averageCompletionTimeMs,
        recentActivity: taskDashboard.recentActivity.slice(0, 10),
      },
      agentEfficiency: agentRanking,
    };
  }

  private json(res: ServerResponse, status: number, data: unknown): void {
    res.writeHead(status, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify(data));
  }

  private readBody(req: IncomingMessage): Promise<Record<string, unknown>> {
    // Return cached body if already pre-read at route level (BUG-003/005)
    const cached: unknown = (req as any).__parsedBody__;
    if (cached !== undefined) {
      if (cached instanceof Error) return Promise.reject(cached);
      return Promise.resolve(cached as Record<string, unknown>);
    }

    return new Promise((resolve, reject) => {
      // BUG-005: Validate Content-Type for POST/PUT/PATCH requests
      const method = req.method ?? '';
      if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
        const contentType = req.headers['content-type'];
        if (!contentType || !contentType.toLowerCase().includes('application/json')) {
          const err = new Error('CONTENT_TYPE_ERROR: Content-Type must be application/json');
          (req as any).__parsedBody__ = err;
          reject(err);
          return;
        }
      }

      const chunks: Buffer[] = [];
      req.on('data', (chunk: Buffer) => chunks.push(chunk));
      req.on('end', () => {
        try {
          const raw = Buffer.concat(chunks).toString();
          const parsed = JSON.parse(raw);
          // BUG-003: JSON literal null or array is not a valid request body
          if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
            const err = new Error('BODY_PARSE_ERROR: Invalid request body');
            (req as any).__parsedBody__ = err;
            reject(err);
            return;
          }
          (req as any).__parsedBody__ = parsed;
          resolve(parsed as Record<string, unknown>);
        } catch {
          // Malformed JSON → treat as empty object (not 400)
          (req as any).__parsedBody__ = {};
          resolve({});
        }
      });
      req.on('error', reject);
    });
  }

  /** Resolve the role directory path for an agent. Uses roleId, normalized role name, or matching by display name. */
  private resolveAgentRoleDir(agent: {
    config: { id: string; roleId?: string };
    role: { name: string };
  }): string | null {
    // Prefer agent's own per-agent role directory (supports self-evolution)
    const agentDataDir = join(this.orgService.getAgentManager().getDataDir(), agent.config.id);
    const agentRoleDir = join(agentDataDir, 'role');
    if (existsSync(join(agentRoleDir, 'ROLE.md'))) return agentRoleDir;

    // Fall back to shared template directory
    const base = join(process.cwd(), 'templates', 'roles');
    if (!existsSync(base)) return null;

    const tryDir = (dirName: string): string | null => {
      const p = join(base, dirName, 'ROLE.md');
      return existsSync(p) ? join(base, dirName) : null;
    };

    if (agent.config.roleId) {
      const d = tryDir(agent.config.roleId);
      if (d) return d;
    }

    const normalized = agent.role.name.toLowerCase().replace(/\s+/g, '-');
    const d = tryDir(normalized);
    if (d) return d;

    for (const entry of readdirSync(base, { withFileTypes: true })) {
      if (!entry.isDirectory()) continue;
      const rolePath = join(base, entry.name, 'ROLE.md');
      if (!existsSync(rolePath)) continue;
      try {
        const content = readFileSync(rolePath, 'utf-8');
        const match = content.match(/^#\s+(.+)$/m);
        const displayName = match?.[1]?.trim();
        if (displayName && displayName.toLowerCase() === agent.role.name.toLowerCase()) {
          return join(base, entry.name);
        }
      } catch {
        /* skip */
      }
    }
    return null;
  }

  private collectStorageInfo(dataDir: string) {
    const dirSize = (p: string, maxDepth = 3, depth = 0): number => {
      if (!existsSync(p)) return 0;
      try {
        const st = statSync(p);
        if (st.isFile()) return st.size;
        if (!st.isDirectory() || depth >= maxDepth) return 0;
        let total = 0;
        for (const entry of readdirSync(p, { withFileTypes: true })) {
          total += dirSize(join(p, entry.name), maxDepth, depth + 1);
        }
        return total;
      } catch { return 0; }
    };

    const topLevelItems: Array<{ name: string; path: string; size: number; description: string }> = [
      { name: 'Database', path: join(dataDir, 'data.db'), size: 0, description: 'SQLite database (tasks, agents, chat, etc.)' },
      { name: 'Agents', path: join(dataDir, 'agents'), size: 0, description: 'Agent workspaces, memory, role files, sessions' },
      { name: 'Skills', path: join(dataDir, 'skills'), size: 0, description: 'Installed skill packages' },
      { name: 'LLM Logs', path: join(dataDir, 'llm-logs'), size: 0, description: 'Daily LLM request/response audit logs' },
      { name: 'Builder Artifacts', path: join(dataDir, 'builder-artifacts'), size: 0, description: 'Agent, team, and skill build outputs' },
      { name: 'Teams', path: join(dataDir, 'teams'), size: 0, description: 'Team announcements and norms' },
      { name: 'Shared', path: join(dataDir, 'shared'), size: 0, description: 'Cross-agent shared files and task deliverables' },
      { name: 'Knowledge', path: join(dataDir, 'knowledge'), size: 0, description: 'File-based knowledge base entries' },
    ];

    for (const item of topLevelItems) {
      if (item.name === 'Database') {
        for (const ext of ['', '-wal', '-shm']) {
          const f = item.path + ext;
          if (existsSync(f)) { try { item.size += statSync(f).size; } catch { /* */ } }
        }
      } else {
        item.size = dirSize(item.path);
      }
    }

    const agentsDir = join(dataDir, 'agents');
    const agentInfos: Array<{ id: string; name: string; size: number; subItems: Array<{ name: string; size: number }> }> = [];
    const am = this.orgService.getAgentManager();

    if (existsSync(agentsDir)) {
      for (const entry of readdirSync(agentsDir, { withFileTypes: true })) {
        if (!entry.isDirectory() || entry.name === 'vector-store') continue;
        const agentDir = join(agentsDir, entry.name);
        const agent = (() => { try { return am.getAgent(entry.name); } catch { return null; } })();
        const subItems = [
          { name: 'workspace', size: dirSize(join(agentDir, 'workspace')) },
          { name: 'memory', size: dirSize(join(agentDir, 'sessions')) + (existsSync(join(agentDir, 'memories.json')) ? statSync(join(agentDir, 'memories.json')).size : 0) + (existsSync(join(agentDir, 'MEMORY.md')) ? statSync(join(agentDir, 'MEMORY.md')).size : 0) },
          { name: 'role', size: dirSize(join(agentDir, 'role')) },
          { name: 'tool-outputs', size: dirSize(join(agentDir, 'tool-outputs')) },
          { name: 'daily-logs', size: dirSize(join(agentDir, 'daily-logs')) },
        ];
        agentInfos.push({
          id: entry.name,
          name: agent?.config?.name ?? entry.name,
          size: subItems.reduce((s, i) => s + i.size, 0),
          subItems,
        });
      }
    }
    agentInfos.sort((a, b) => b.size - a.size);

    const totalSize = topLevelItems.reduce((s, i) => s + i.size, 0);
    const dbItem = topLevelItems.find(i => i.name === 'Database')!;

    return {
      dataDir,
      totalSize,
      breakdown: topLevelItems,
      agents: agentInfos,
      database: { path: dbItem.path, size: dbItem.size },
    };
  }

  private detectOrphans() {
    const dataDir = join(homedir(), '.markus');
    const am = this.orgService.getAgentManager();
    const knownAgentIds = new Set(am.listAgents().map(a => a.id));
    const teams = this.orgService.listTeams('default');
    const knownTeamIds = new Set(teams.map(t => t.id));

    const orphanAgents: Array<{ id: string; path: string; size: number }> = [];
    const orphanTeams: Array<{ id: string; path: string; size: number }> = [];

    const dirSize = (p: string, maxDepth = 3, depth = 0): number => {
      if (!existsSync(p)) return 0;
      try {
        const st = statSync(p);
        if (st.isFile()) return st.size;
        if (!st.isDirectory() || depth >= maxDepth) return 0;
        let total = 0;
        for (const entry of readdirSync(p, { withFileTypes: true })) {
          total += dirSize(join(p, entry.name), maxDepth, depth + 1);
        }
        return total;
      } catch { return 0; }
    };

    const agentsDir = join(dataDir, 'agents');
    if (existsSync(agentsDir)) {
      for (const entry of readdirSync(agentsDir, { withFileTypes: true })) {
        if (!entry.isDirectory() || entry.name === 'vector-store') continue;
        if (!knownAgentIds.has(entry.name)) {
          const p = join(agentsDir, entry.name);
          orphanAgents.push({ id: entry.name, path: p, size: dirSize(p) });
        }
      }
    }

    const teamsDir = join(dataDir, 'teams');
    if (existsSync(teamsDir)) {
      for (const entry of readdirSync(teamsDir, { withFileTypes: true })) {
        if (!entry.isDirectory()) continue;
        if (!knownTeamIds.has(entry.name)) {
          const p = join(teamsDir, entry.name);
          orphanTeams.push({ id: entry.name, path: p, size: dirSize(p) });
        }
      }
    }

    return {
      orphanAgents: orphanAgents.sort((a, b) => b.size - a.size),
      orphanTeams: orphanTeams.sort((a, b) => b.size - a.size),
      totalOrphanSize: [...orphanAgents, ...orphanTeams].reduce((s, o) => s + o.size, 0),
    };
  }

  private purgeOrphans(ids?: string[]) {
    const orphans = this.detectOrphans();
    const filter = ids && ids.length > 0 ? new Set(ids) : null;
    const purgedAgents: string[] = [];
    const purgedTeams: string[] = [];
    const failures: string[] = [];

    for (const o of orphans.orphanAgents) {
      if (filter && !filter.has(o.id)) continue;
      try {
        rmSync(o.path, { recursive: true, force: true });
        purgedAgents.push(o.id);
      } catch { failures.push(o.id); }
    }

    for (const o of orphans.orphanTeams) {
      if (filter && !filter.has(o.id)) continue;
      try {
        rmSync(o.path, { recursive: true, force: true });
        purgedTeams.push(o.id);
      } catch { failures.push(o.id); }
    }

    return {
      purgedAgents,
      purgedTeams,
      freedBytes: orphans.totalOrphanSize,
      failures,
    };
  }
}

function stripHtmlTags(value: string): string {
  return value.replace(/<[^>]*>/g, '');
}
