const BASE = '/api' async function get(path: string): Promise { const res = await fetch(`${BASE}${path}`) if (!res.ok) throw new Error(`${res.status} ${res.statusText}`) return res.json() } async function patch(path: string, body: unknown): Promise { const res = await fetch(`${BASE}${path}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) if (!res.ok) throw new Error(`${res.status} ${res.statusText}`) return res.json() } async function post(path: string, body: unknown): Promise { const res = await fetch(`${BASE}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) if (!res.ok) throw new Error(`${res.status} ${res.statusText}`) return res.json() } async function del(path: string): Promise { const res = await fetch(`${BASE}${path}`, { method: 'DELETE' }) if (!res.ok) throw new Error(`${res.status} ${res.statusText}`) return res.json() } export interface Project { id: string name: string path: string status: string priority: number tech_stack: string[] | null execution_mode: string | null autocommit_enabled: number | null obsidian_vault_path: string | null deploy_command: string | null created_at: string total_tasks: number done_tasks: number active_tasks: number blocked_tasks: number review_tasks: number project_type: string | null ssh_host: string | null ssh_user: string | null ssh_key_path: string | null ssh_proxy_jump: string | null description: string | null } export interface ObsidianSyncResult { exported_decisions: number tasks_updated: number errors: string[] vault_path: string } export interface ProjectDetail extends Project { tasks: Task[] modules: Module[] decisions: Decision[] } export interface Task { id: string project_id: string title: string status: string priority: number assigned_role: string | null parent_task_id: string | null brief: Record | null spec: Record | null execution_mode: string | null blocked_reason: string | null dangerously_skipped: number | null category: string | null acceptance_criteria: string | null created_at: string updated_at: string } export interface Decision { id: number project_id: string task_id: string | null type: string category: string | null title: string description: string tags: string[] | null created_at: string } export interface Module { id: number project_id: string name: string type: string path: string description: string | null owner_role: string | null dependencies: string[] | null } export interface PipelineStep { id: number agent_role: string action: string output_summary: string | null success: boolean | number duration_seconds: number | null tokens_used: number | null model: string | null cost_usd: number | null created_at: string } export interface DeployResult { success: boolean exit_code: number stdout: string stderr: string duration_seconds: number } export interface TaskFull extends Task { pipeline_steps: PipelineStep[] related_decisions: Decision[] project_deploy_command: string | null } export interface PendingAction { type: string description: string original_item: Record options: string[] } export interface CostEntry { project_id: string project_name: string runs: number total_tokens: number total_cost_usd: number total_duration_seconds: number } export interface Phase { id: number project_id: string role: string phase_order: number status: string task_id: string | null revise_count: number revise_comment: string | null created_at: string updated_at: string task?: Task | null } export interface NewProjectPayload { id: string name: string path: string description: string roles: string[] tech_stack?: string[] priority?: number language?: string project_type?: string ssh_host?: string ssh_user?: string ssh_key_path?: string ssh_proxy_jump?: string } export interface NewProjectResult { project: Project phases: Phase[] } export interface AuditItem { id: string reason: string } export interface AuditResult { success: boolean already_done: AuditItem[] still_pending: AuditItem[] unclear: AuditItem[] duration_seconds?: number cost_usd?: number error?: string } export interface EscalationNotification { task_id: string project_id: string agent_role: string reason: string pipeline_step: string | null blocked_at: string telegram_sent: boolean } export const api = { projects: () => get('/projects'), project: (id: string) => get(`/projects/${id}`), task: (id: string) => get(`/tasks/${id}`), taskFull: (id: string) => get(`/tasks/${id}/full`), taskPipeline: (id: string) => get(`/tasks/${id}/pipeline`), cost: (days = 7) => get(`/cost?days=${days}`), createProject: (data: { id: string; name: string; path?: string; tech_stack?: string[]; priority?: number; project_type?: string; ssh_host?: string; ssh_user?: string; ssh_key_path?: string; ssh_proxy_jump?: string }) => post('/projects', data), createTask: (data: { project_id: string; title: string; priority?: number; route_type?: string; category?: string; acceptance_criteria?: string }) => post('/tasks', data), approveTask: (id: string, data?: { decision_title?: string; decision_description?: string; decision_type?: string; create_followups?: boolean }) => post<{ status: string; followup_tasks: Task[]; needs_decision: boolean; pending_actions: PendingAction[] }>(`/tasks/${id}/approve`, data || {}), resolveAction: (id: string, action: PendingAction, choice: string) => post<{ choice: string; result: unknown }>(`/tasks/${id}/resolve`, { action, choice }), rejectTask: (id: string, reason: string) => post<{ status: string }>(`/tasks/${id}/reject`, { reason }), reviseTask: (id: string, comment: string) => post<{ status: string; comment: string }>(`/tasks/${id}/revise`, { comment }), runTask: (id: string) => post<{ status: string }>(`/tasks/${id}/run`, {}), bootstrap: (data: { path: string; id: string; name: string }) => post<{ project: Project }>('/bootstrap', data), auditProject: (projectId: string) => post(`/projects/${projectId}/audit`, {}), auditApply: (projectId: string, taskIds: string[]) => post<{ updated: string[]; count: number }>(`/projects/${projectId}/audit/apply`, { task_ids: taskIds }), patchTask: (id: string, data: { status?: string; execution_mode?: string; priority?: number; route_type?: string; title?: string; brief_text?: string; acceptance_criteria?: string }) => patch(`/tasks/${id}`, data), patchProject: (id: string, data: { execution_mode?: string; autocommit_enabled?: boolean; obsidian_vault_path?: string; deploy_command?: string; project_type?: string; ssh_host?: string; ssh_user?: string; ssh_key_path?: string; ssh_proxy_jump?: string }) => patch(`/projects/${id}`, data), deployProject: (projectId: string) => post(`/projects/${projectId}/deploy`, {}), syncObsidian: (projectId: string) => post(`/projects/${projectId}/sync/obsidian`, {}), deleteDecision: (projectId: string, decisionId: number) => del<{ deleted: number }>(`/projects/${projectId}/decisions/${decisionId}`), createDecision: (data: { project_id: string; type: string; title: string; description: string; category?: string; tags?: string[] }) => post('/decisions', data), newProject: (data: NewProjectPayload) => post('/projects/new', data), getPhases: (projectId: string) => get(`/projects/${projectId}/phases`), notifications: (projectId?: string) => get(`/notifications${projectId ? `?project_id=${projectId}` : ''}`), approvePhase: (phaseId: number, comment?: string) => post<{ phase: Phase; next_phase: Phase | null }>(`/phases/${phaseId}/approve`, { comment }), rejectPhase: (phaseId: number, reason: string) => post(`/phases/${phaseId}/reject`, { reason }), revisePhase: (phaseId: number, comment: string) => post<{ phase: Phase; new_task: Task }>(`/phases/${phaseId}/revise`, { comment }), startPhase: (projectId: string) => post<{ status: string; phase_id: number; task_id: string }>(`/projects/${projectId}/phases/start`, {}), }