Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions messages/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,8 @@
"colInProgress": "قيد التنفيذ",
"colReview": "المراجعة",
"colQualityReview": "مراجعة الجودة",
"colFailed": "فشل",
"retryTask": "إعادة المحاولة",
"colDone": "مكتمل",
"recurring": "متكرر",
"spawned": "مُنشأ",
Expand Down
2 changes: 2 additions & 0 deletions messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,8 @@
"colInProgress": "In Bearbeitung",
"colReview": "Überprüfung",
"colQualityReview": "Qualitätsprüfung",
"colFailed": "Fehlgeschlagen",
"retryTask": "Erneut versuchen",
"colDone": "Erledigt",
"recurring": "WIEDERKEHREND",
"spawned": "ERSTELLT",
Expand Down
2 changes: 2 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,8 @@
"colInProgress": "In Progress",
"colReview": "Review",
"colQualityReview": "Quality Review",
"colFailed": "Failed",
"retryTask": "Retry Task",
"colDone": "Done",
"recurring": "RECURRING",
"spawned": "SPAWNED",
Expand Down
2 changes: 2 additions & 0 deletions messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,8 @@
"colInProgress": "En progreso",
"colReview": "Revisión",
"colQualityReview": "Revisión de calidad",
"colFailed": "Fallido",
"retryTask": "Reintentar",
"colDone": "Hecho",
"recurring": "RECURRENTE",
"spawned": "GENERADO",
Expand Down
2 changes: 2 additions & 0 deletions messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,8 @@
"colInProgress": "En cours",
"colReview": "Révision",
"colQualityReview": "Révision qualité",
"colFailed": "Échoué",
"retryTask": "Réessayer",
"colDone": "Terminé",
"recurring": "RÉCURRENT",
"spawned": "GÉNÉRÉ",
Expand Down
2 changes: 2 additions & 0 deletions messages/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,8 @@
"colInProgress": "進行中",
"colReview": "レビュー",
"colQualityReview": "品質レビュー",
"colFailed": "失敗",
"retryTask": "再試行",
"colDone": "完了",
"recurring": "定期",
"spawned": "生成済み",
Expand Down
2 changes: 2 additions & 0 deletions messages/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,8 @@
"colInProgress": "진행 중",
"colReview": "검토",
"colQualityReview": "품질 검토",
"colFailed": "실패",
"retryTask": "재시도",
"colDone": "완료",
"recurring": "반복",
"spawned": "생성됨",
Expand Down
2 changes: 2 additions & 0 deletions messages/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,8 @@
"colInProgress": "Em andamento",
"colReview": "Revisão",
"colQualityReview": "Revisão de qualidade",
"colFailed": "Falhou",
"retryTask": "Tentar novamente",
"colDone": "Concluído",
"recurring": "RECORRENTE",
"spawned": "GERADO",
Expand Down
2 changes: 2 additions & 0 deletions messages/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,8 @@
"colInProgress": "В работе",
"colReview": "На проверке",
"colQualityReview": "Контроль качества",
"colFailed": "Ошибка",
"retryTask": "Повторить",
"colDone": "Готово",
"recurring": "ПОВТОРЯЮЩАЯСЯ",
"spawned": "СОЗДАНА",
Expand Down
2 changes: 2 additions & 0 deletions messages/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,8 @@
"colInProgress": "进行中",
"colReview": "审查",
"colQualityReview": "质量审查",
"colFailed": "失败",
"retryTask": "重试",
"colDone": "完成",
"recurring": "循环",
"spawned": "已生成",
Expand Down
29 changes: 26 additions & 3 deletions scripts/mc-mcp-server.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -298,9 +298,32 @@ const TOOLS = [
// --- Tasks ---
{
name: 'mc_list_tasks',
description: 'List all tasks in Mission Control',
inputSchema: { type: 'object', properties: {}, required: [] },
handler: async () => api('GET', '/api/tasks'),
description: 'List tasks in Mission Control with optional filters',
inputSchema: {
type: 'object',
properties: {
status: { type: 'string', description: 'Filter by status: backlog, inbox, assigned, awaiting_owner, in_progress, review, quality_review, done, failed' },
assigned_to: { type: 'string', description: 'Filter by assigned agent name' },
priority: { type: 'string', description: 'Filter by priority: low, medium, high, critical' },
search: { type: 'string', description: 'Search in task title (partial match)' },
limit: { type: 'number', description: 'Max results (default 50, max 200)' },
},
required: [],
},
handler: async ({ status, assigned_to, priority, search, limit } = {}) => {
const params = new URLSearchParams()
if (status) params.set('status', status)
if (assigned_to) params.set('assigned_to', assigned_to)
if (priority) params.set('priority', priority)
if (limit) params.set('limit', String(Math.min(limit, 200)))
const qs = params.toString() ? `?${params.toString()}` : ''
const result = await api('GET', `/api/tasks${qs}`)
if (search && result?.tasks) {
const term = search.toLowerCase()
result.tasks = result.tasks.filter(t => t.title?.toLowerCase().includes(term))
}
return result
},
},
{
name: 'mc_get_task',
Expand Down
2 changes: 1 addition & 1 deletion scripts/mc-tui.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -917,7 +917,7 @@ async function handleInputKey(key, str, render) {
render();
setTimeout(() => { state.actionMessage = ''; render(); }, 2000);
} else if (state.inputMode === 'edit-status') {
const valid = ['backlog', 'inbox', 'assigned', 'in_progress', 'review', 'done', 'failed'];
const valid = ['backlog', 'inbox', 'assigned', 'awaiting_owner', 'in_progress', 'review', 'done', 'failed'];
if (!valid.includes(value)) {
state.actionMessage = `Invalid status. Use: ${valid.join(', ')}`;
state.inputMode = null;
Expand Down
27 changes: 5 additions & 22 deletions src/app/api/tasks/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { logger } from '@/lib/logger';
import { validateBody, updateTaskSchema } from '@/lib/validation';
import { resolveMentionRecipients } from '@/lib/mentions';
import { normalizeTaskUpdateStatus } from '@/lib/task-status';
import { pushTaskToGitHub } from '@/lib/github-sync-engine';
import { pushTaskToGnap, removeTaskFromGnap } from '@/lib/gnap-sync';
import { syncTaskOutbound } from '@/lib/github-sync-engine';
import { removeTaskFromGnap } from '@/lib/gnap-sync';
import { config } from '@/lib/config';

function formatTicketRef(prefix?: string | null, num?: number | null): string | undefined {
Expand Down Expand Up @@ -391,26 +391,9 @@ export async function PUT(
`).get(taskId, workspaceId) as Task;
const parsedTask = mapTaskRow(updatedTask);

// Fire-and-forget outbound GitHub sync for relevant changes
const syncRelevantChanges = changes.some(c =>
c.startsWith('status:') || c.startsWith('priority:') || c.includes('title') || c.includes('assigned')
)
if (syncRelevantChanges && (updatedTask as any).github_repo) {
const project = db.prepare(`
SELECT id, github_repo, github_sync_enabled FROM projects
WHERE id = ? AND workspace_id = ?
`).get((updatedTask as any).project_id, workspaceId) as any
if (project?.github_sync_enabled) {
pushTaskToGitHub(updatedTask as any, project).catch(err =>
logger.error({ err, taskId }, 'Outbound GitHub sync failed')
)
}
}

// Fire-and-forget GNAP sync for task updates
if (config.gnap.enabled && config.gnap.autoSync && changes.length > 0) {
try { pushTaskToGnap(updatedTask as any, config.gnap.repoPath) }
catch (err) { logger.warn({ err, taskId }, 'GNAP sync failed for task update') }
// Fire-and-forget outbound sync (GitHub + GNAP)
if (changes.length > 0) {
syncTaskOutbound(updatedTask as any, workspaceId);
}

// Broadcast to SSE clients
Expand Down
10 changes: 8 additions & 2 deletions src/app/api/tasks/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { logger } from '@/lib/logger';
import { validateBody, createTaskSchema, bulkUpdateTaskStatusSchema } from '@/lib/validation';
import { resolveMentionRecipients } from '@/lib/mentions';
import { normalizeTaskCreateStatus } from '@/lib/task-status';
import { pushTaskToGitHub } from '@/lib/github-sync-engine';
import { pushTaskToGitHub, syncTaskOutbound } from '@/lib/github-sync-engine';
import { pushTaskToGnap } from '@/lib/gnap-sync';
import { config } from '@/lib/config';

Expand Down Expand Up @@ -397,13 +397,19 @@ export async function PUT(request: NextRequest) {

transaction(tasks);

// Broadcast status changes to SSE clients
// Broadcast status changes to SSE clients + outbound sync
for (const task of tasks) {
eventBus.broadcast('task.status_changed', {
id: task.id,
status: task.status,
updated_at: Math.floor(Date.now() / 1000),
});

// Fire-and-forget outbound sync (GitHub + GNAP)
const fullTask = db.prepare('SELECT * FROM tasks WHERE id = ? AND workspace_id = ?').get(task.id, workspaceId) as Task | undefined;
if (fullTask) {
syncTaskOutbound(fullTask as any, workspaceId);
}
}

return NextResponse.json({ success: true, updated: tasks.length });
Expand Down
32 changes: 31 additions & 1 deletion src/components/panels/task-board-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ interface Task {
id: number
title: string
description?: string
status: 'backlog' | 'inbox' | 'assigned' | 'in_progress' | 'review' | 'quality_review' | 'done' | 'awaiting_owner'
status: 'backlog' | 'inbox' | 'assigned' | 'awaiting_owner' | 'in_progress' | 'review' | 'quality_review' | 'done' | 'failed'
priority: 'low' | 'medium' | 'high' | 'critical' | 'urgent'
assigned_to?: string
created_by: string
Expand All @@ -45,6 +45,8 @@ interface Task {
github_pr_number?: number
github_pr_state?: string
comment_count?: number
error_message?: string
dispatch_attempts?: number
}

interface Agent {
Expand Down Expand Up @@ -96,6 +98,7 @@ const STATUS_COLUMN_KEYS = [
{ key: 'review', titleKey: 'colReview', color: 'bg-purple-500/20 text-purple-400' },
{ key: 'quality_review', titleKey: 'colQualityReview', color: 'bg-indigo-500/20 text-indigo-400' },
{ key: 'done', titleKey: 'colDone', color: 'bg-green-500/20 text-green-400' },
{ key: 'failed', titleKey: 'colFailed', color: 'bg-red-500/20 text-red-400' },
]

const AWAITING_OWNER_KEYWORDS = [
Expand Down Expand Up @@ -1485,6 +1488,33 @@ function TaskDetailModal({
)}
</div>

{/* Failed task: error message + retry button */}
{task.status === 'failed' && (
<div className="mx-6 mb-2 p-3 rounded-lg border border-red-500/20 bg-red-500/5 space-y-2">
{task.error_message && (
<p className="text-xs text-red-400 font-mono whitespace-pre-wrap">{task.error_message}</p>
)}
{task.dispatch_attempts != null && task.dispatch_attempts > 0 && (
<p className="text-2xs text-muted-foreground">Dispatch attempts: {task.dispatch_attempts}</p>
)}
<button
onClick={async () => {
try {
const res = await fetch(`/api/tasks/${task.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: 'assigned', dispatch_attempts: 0, error_message: null }),
})
if (res.ok) onClose()
} catch { /* ignore */ }
}}
className="text-xs px-3 py-1.5 rounded bg-blue-500/20 text-blue-400 hover:bg-blue-500/30 border border-blue-500/30 transition-colors"
>
{t('retryTask')}
</button>
</div>
)}

{/* Content */}
<div className="px-6 py-4">
<div className="flex gap-1.5 mb-4" role="tablist" aria-label={t('taskDetailTabs')}>
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export interface Task {
id: number
title: string
description?: string
status: 'backlog' | 'inbox' | 'assigned' | 'in_progress' | 'review' | 'quality_review' | 'done'
status: 'backlog' | 'inbox' | 'assigned' | 'awaiting_owner' | 'in_progress' | 'review' | 'quality_review' | 'done' | 'failed'
priority: 'low' | 'medium' | 'high' | 'urgent'
project_id?: number
project_ticket_no?: number
Expand Down
8 changes: 4 additions & 4 deletions src/lib/__tests__/github-label-map.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ describe('labelToStatus', () => {
})

it('is the inverse of statusToLabel', () => {
const statuses = ['backlog', 'inbox', 'assigned', 'in_progress', 'review', 'quality_review', 'done'] as const
const statuses = ['backlog', 'inbox', 'assigned', 'awaiting_owner', 'in_progress', 'review', 'quality_review', 'done', 'failed'] as const
for (const status of statuses) {
expect(labelToStatus(statusToLabel(status).name)).toBe(status)
}
Expand Down Expand Up @@ -88,7 +88,7 @@ describe('labelToPriority', () => {

describe('ALL_MC_LABELS', () => {
it('contains all status and priority labels', () => {
expect(ALL_MC_LABELS.length).toBe(11) // 7 statuses + 4 priorities
expect(ALL_MC_LABELS.length).toBe(13) // 9 statuses + 4 priorities
const names = ALL_MC_LABELS.map(l => l.name)
expect(names).toContain('mc:inbox')
expect(names).toContain('priority:critical')
Expand All @@ -103,8 +103,8 @@ describe('ALL_MC_LABELS', () => {
})

describe('ALL_STATUS_LABEL_NAMES', () => {
it('contains all 7 status label names', () => {
expect(ALL_STATUS_LABEL_NAMES).toHaveLength(7)
it('contains all 9 status label names', () => {
expect(ALL_STATUS_LABEL_NAMES).toHaveLength(9)
expect(ALL_STATUS_LABEL_NAMES).toContain('mc:inbox')
expect(ALL_STATUS_LABEL_NAMES).toContain('mc:done')
})
Expand Down
2 changes: 1 addition & 1 deletion src/lib/__tests__/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('createTaskSchema', () => {
})

it('accepts all valid statuses', () => {
for (const status of ['backlog', 'inbox', 'assigned', 'in_progress', 'review', 'quality_review', 'done']) {
for (const status of ['backlog', 'inbox', 'assigned', 'awaiting_owner', 'in_progress', 'review', 'quality_review', 'done', 'failed']) {
const result = createTaskSchema.safeParse({ title: 'T', status })
expect(result.success).toBe(true)
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ export interface Task {
id: number;
title: string;
description?: string;
status: 'backlog' | 'inbox' | 'assigned' | 'in_progress' | 'review' | 'quality_review' | 'done';
status: 'backlog' | 'inbox' | 'assigned' | 'awaiting_owner' | 'in_progress' | 'review' | 'quality_review' | 'done' | 'failed';
priority: 'low' | 'medium' | 'high' | 'urgent';
project_id?: number;
project_ticket_no?: number;
Expand Down
1 change: 1 addition & 0 deletions src/lib/event-bus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export type EventType =
| 'run.updated'
| 'run.completed'
| 'run.eval_attached'
| 'task.escalated'

class ServerEventBus extends EventEmitter {
private static instance: ServerEventBus | null = null
Expand Down
4 changes: 3 additions & 1 deletion src/lib/github-label-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Labels use `mc:` prefix to avoid collisions with existing repo labels.
*/

export type TaskStatus = 'backlog' | 'inbox' | 'assigned' | 'in_progress' | 'review' | 'quality_review' | 'done'
export type TaskStatus = 'backlog' | 'inbox' | 'assigned' | 'awaiting_owner' | 'in_progress' | 'review' | 'quality_review' | 'done' | 'failed'
export type TaskPriority = 'low' | 'medium' | 'high' | 'critical'

interface LabelDef {
Expand All @@ -22,6 +22,8 @@ const STATUS_LABEL_MAP: Record<TaskStatus, LabelDef> = {
review: { name: 'mc:review', color: 'a855f7', description: 'Mission Control: review' },
quality_review: { name: 'mc:quality-review', color: '6366f1', description: 'Mission Control: quality review' },
done: { name: 'mc:done', color: '22c55e', description: 'Mission Control: done' },
awaiting_owner: { name: 'mc:awaiting-owner', color: 'f97316', description: 'Mission Control: awaiting owner' },
failed: { name: 'mc:failed', color: 'ef4444', description: 'Mission Control: failed' },
}

const LABEL_STATUS_MAP: Record<string, TaskStatus> = Object.fromEntries(
Expand Down
37 changes: 37 additions & 0 deletions src/lib/github-sync-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,40 @@ export async function pullFromGitHub(

return { pulled, pushed }
}

/**
* Fire-and-forget outbound sync for a task to GitHub + GNAP.
* Called after any status change — drag-drop, dispatch, Aegis, requeue.
*/
export function syncTaskOutbound(
task: { id: number; title: string; status: string; priority: string; description?: string | null; github_issue_number?: number | null; github_repo?: string | null; project_id?: number | null; workspace_id?: number },
workspaceId: number
): void {
const db = getDatabase()
try {
// GitHub sync
if (task.project_id) {
const project = db.prepare(
'SELECT id, github_repo, github_sync_enabled FROM projects WHERE id = ? AND workspace_id = ?'
).get(task.project_id, workspaceId) as { id: number; github_repo?: string | null; github_sync_enabled?: number | null } | undefined
if (project?.github_sync_enabled) {
pushTaskToGitHub(task as any, project).catch(err =>
logger.warn({ err, taskId: task.id }, 'Outbound GitHub sync failed')
)
}
}
} catch (err) {
logger.warn({ err, taskId: task.id }, 'GitHub sync lookup failed')
}

try {
// GNAP sync
const { config } = require('@/lib/config')
if (config.gnap?.enabled && config.gnap?.repoPath) {
const { pushTaskToGnap } = require('@/lib/gnap-sync')
pushTaskToGnap(task, config.gnap.repoPath)
}
} catch (err) {
logger.warn({ err, taskId: task.id }, 'GNAP sync failed')
}
}
2 changes: 2 additions & 0 deletions src/lib/gnap-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ const MC_TO_GNAP_STATUS: Record<string, string> = {
quality_review: 'review',
completed: 'done',
done: 'done',
awaiting_owner: 'blocked',
failed: 'blocked',
blocked: 'blocked',
cancelled: 'cancelled',
}
Expand Down
Loading
Loading