Remove task-level approval system

Task-level approval (requiresApproval, mergeRequiresApproval,
pending_approval status) was redundant with executionMode
(yolo vs review_per_phase) and blocked the orchestrator's
phase completion flow. Tasks now complete directly;
phase-level review via executionMode is the right granularity.

Removed: schema columns (left in DB, removed from Drizzle),
TaskPendingApprovalEvent, approveTask/listPendingApprovals
procedures, findPendingApproval repository method, and all
frontend approval UI.
This commit is contained in:
Lukas May
2026-03-05 17:09:48 +01:00
parent 209629241d
commit 8804455c77
27 changed files with 48 additions and 237 deletions

View File

@@ -48,7 +48,6 @@ describe('writeInputFiles', () => {
id: 'init-1', id: 'init-1',
name: 'Test Initiative', name: 'Test Initiative',
status: 'active', status: 'active',
mergeRequiresApproval: true,
branch: 'cw/test-initiative', branch: 'cw/test-initiative',
executionMode: 'review_per_phase', executionMode: 'review_per_phase',
createdAt: new Date('2026-01-01'), createdAt: new Date('2026-01-01'),

View File

@@ -132,7 +132,6 @@ export async function writeInputFiles(options: WriteInputFilesOptions): Promise<
id: ini.id, id: ini.id,
name: ini.name, name: ini.name,
status: ini.status, status: ini.status,
mergeRequiresApproval: ini.mergeRequiresApproval,
branch: ini.branch, branch: ini.branch,
}, },
'', '',

View File

@@ -336,7 +336,7 @@ export function createCli(serverHandler?: (port?: number) => Promise<void>): Com
} }
// Count by status // Count by status
const pending = tasks.filter(t => t.status === 'pending' || t.status === 'pending_approval').length; const pending = tasks.filter(t => t.status === 'pending').length;
const inProgress = tasks.filter(t => t.status === 'in_progress').length; const inProgress = tasks.filter(t => t.status === 'in_progress').length;
const completed = tasks.filter(t => t.status === 'completed').length; const completed = tasks.filter(t => t.status === 'completed').length;
const blocked = tasks.filter(t => t.status === 'blocked').length; const blocked = tasks.filter(t => t.status === 'blocked').length;

View File

@@ -4,7 +4,7 @@
* Implements TaskRepository interface using Drizzle ORM. * Implements TaskRepository interface using Drizzle ORM.
*/ */
import { eq, asc, and } from 'drizzle-orm'; import { eq, asc } from 'drizzle-orm';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import type { DrizzleDatabase } from '../../index.js'; import type { DrizzleDatabase } from '../../index.js';
import { tasks, taskDependencies, type Task } from '../../schema.js'; import { tasks, taskDependencies, type Task } from '../../schema.js';
@@ -12,7 +12,6 @@ import type {
TaskRepository, TaskRepository,
CreateTaskData, CreateTaskData,
UpdateTaskData, UpdateTaskData,
PendingApprovalFilters,
} from '../task-repository.js'; } from '../task-repository.js';
/** /**
@@ -77,26 +76,6 @@ export class DrizzleTaskRepository implements TaskRepository {
.orderBy(asc(tasks.order)); .orderBy(asc(tasks.order));
} }
async findPendingApproval(filters?: PendingApprovalFilters): Promise<Task[]> {
const conditions = [eq(tasks.status, 'pending_approval')];
if (filters?.initiativeId) {
conditions.push(eq(tasks.initiativeId, filters.initiativeId));
}
if (filters?.phaseId) {
conditions.push(eq(tasks.phaseId, filters.phaseId));
}
if (filters?.category) {
conditions.push(eq(tasks.category, filters.category));
}
return this.db
.select()
.from(tasks)
.where(and(...conditions))
.orderBy(asc(tasks.createdAt));
}
async update(id: string, data: UpdateTaskData): Promise<Task> { async update(id: string, data: UpdateTaskData): Promise<Task> {
const [updated] = await this.db const [updated] = await this.db
.update(tasks) .update(tasks)

View File

@@ -22,7 +22,6 @@ export type {
TaskRepository, TaskRepository,
CreateTaskData, CreateTaskData,
UpdateTaskData, UpdateTaskData,
PendingApprovalFilters,
} from './task-repository.js'; } from './task-repository.js';
export type { export type {

View File

@@ -20,15 +20,6 @@ export type CreateTaskData = Omit<NewTask, 'id' | 'createdAt' | 'updatedAt'> & {
*/ */
export type UpdateTaskData = Partial<CreateTaskData>; export type UpdateTaskData = Partial<CreateTaskData>;
/**
* Filters for finding pending approval tasks.
*/
export interface PendingApprovalFilters {
initiativeId?: string;
phaseId?: string;
category?: TaskCategory;
}
/** /**
* Task Repository Port * Task Repository Port
* *
@@ -70,13 +61,6 @@ export interface TaskRepository {
*/ */
findByPhaseId(phaseId: string): Promise<Task[]>; findByPhaseId(phaseId: string): Promise<Task[]>;
/**
* Find all tasks with status 'pending_approval'.
* Optional filters by initiative, phase, or category.
* Returns tasks ordered by createdAt.
*/
findPendingApproval(filters?: PendingApprovalFilters): Promise<Task[]>;
/** /**
* Update a task. * Update a task.
* Throws if task not found. * Throws if task not found.

View File

@@ -22,9 +22,6 @@ export const initiatives = sqliteTable('initiatives', {
status: text('status', { enum: ['active', 'completed', 'archived', 'pending_review'] }) status: text('status', { enum: ['active', 'completed', 'archived', 'pending_review'] })
.notNull() .notNull()
.default('active'), .default('active'),
mergeRequiresApproval: integer('merge_requires_approval', { mode: 'boolean' })
.notNull()
.default(true),
branch: text('branch'), // Auto-generated initiative branch (e.g., 'cw/user-auth') branch: text('branch'), // Auto-generated initiative branch (e.g., 'cw/user-auth')
executionMode: text('execution_mode', { enum: ['yolo', 'review_per_phase'] }) executionMode: text('execution_mode', { enum: ['yolo', 'review_per_phase'] })
.notNull() .notNull()
@@ -153,11 +150,10 @@ export const tasks = sqliteTable('tasks', {
.notNull() .notNull()
.default('medium'), .default('medium'),
status: text('status', { status: text('status', {
enum: ['pending_approval', 'pending', 'in_progress', 'completed', 'blocked'], enum: ['pending', 'in_progress', 'completed', 'blocked'],
}) })
.notNull() .notNull()
.default('pending'), .default('pending'),
requiresApproval: integer('requires_approval', { mode: 'boolean' }), // null = inherit from initiative
order: integer('order').notNull().default(0), order: integer('order').notNull().default(0),
summary: text('summary'), // Agent result summary — propagated to dependent tasks as context summary: text('summary'), // Agent result summary — propagated to dependent tasks as context
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),

View File

@@ -13,7 +13,6 @@ import type {
TaskCompletedEvent, TaskCompletedEvent,
TaskBlockedEvent, TaskBlockedEvent,
TaskDispatchedEvent, TaskDispatchedEvent,
TaskPendingApprovalEvent,
} from '../events/index.js'; } from '../events/index.js';
import type { AgentManager, AgentResult, AgentInfo } from '../agent/types.js'; import type { AgentManager, AgentResult, AgentInfo } from '../agent/types.js';
import type { TaskRepository } from '../db/repositories/task-repository.js'; import type { TaskRepository } from '../db/repositories/task-repository.js';
@@ -172,7 +171,6 @@ export class DefaultDispatchManager implements DispatchManager {
/** /**
* Mark a task as complete. * Mark a task as complete.
* If the task requires approval, sets status to 'pending_approval' instead.
* Updates task status and removes from queue. * Updates task status and removes from queue.
* *
* @param taskId - ID of the task to complete * @param taskId - ID of the task to complete
@@ -184,78 +182,15 @@ export class DefaultDispatchManager implements DispatchManager {
throw new Error(`Task not found: ${taskId}`); throw new Error(`Task not found: ${taskId}`);
} }
// Determine if approval is required
const requiresApproval = await this.taskRequiresApproval(task);
// Store agent result summary on the task for propagation to dependent tasks // Store agent result summary on the task for propagation to dependent tasks
await this.storeAgentSummary(taskId, agentId); await this.storeAgentSummary(taskId, agentId);
if (requiresApproval) {
// Set to pending_approval instead of completed
await this.taskRepository.update(taskId, { status: 'pending_approval' });
// Remove from queue
this.taskQueue.delete(taskId);
log.info({ taskId, category: task.category }, 'task pending approval');
// Emit TaskPendingApprovalEvent
const event: TaskPendingApprovalEvent = {
type: 'task:pending_approval',
timestamp: new Date(),
payload: {
taskId,
agentId: agentId ?? '',
category: task.category,
name: task.name,
},
};
this.eventBus.emit(event);
} else {
// Complete directly
await this.taskRepository.update(taskId, { status: 'completed' });
// Remove from queue
this.taskQueue.delete(taskId);
log.info({ taskId }, 'task completed');
// Emit TaskCompletedEvent
const event: TaskCompletedEvent = {
type: 'task:completed',
timestamp: new Date(),
payload: {
taskId,
agentId: agentId ?? '',
success: true,
message: 'Task completed',
},
};
this.eventBus.emit(event);
}
// Also remove from blocked if it was there
this.blockedTasks.delete(taskId);
}
/**
* Approve a task that is pending approval.
* Sets status to 'completed' and emits completion event.
*/
async approveTask(taskId: string): Promise<void> {
const task = await this.taskRepository.findById(taskId);
if (!task) {
throw new Error(`Task not found: ${taskId}`);
}
if (task.status !== 'pending_approval') {
throw new Error(`Task ${taskId} is not pending approval (status: ${task.status})`);
}
// Complete the task
await this.taskRepository.update(taskId, { status: 'completed' }); await this.taskRepository.update(taskId, { status: 'completed' });
log.info({ taskId }, 'task approved and completed'); // Remove from queue
this.taskQueue.delete(taskId);
log.info({ taskId }, 'task completed');
// Emit TaskCompletedEvent // Emit TaskCompletedEvent
const event: TaskCompletedEvent = { const event: TaskCompletedEvent = {
@@ -263,12 +198,15 @@ export class DefaultDispatchManager implements DispatchManager {
timestamp: new Date(), timestamp: new Date(),
payload: { payload: {
taskId, taskId,
agentId: '', agentId: agentId ?? '',
success: true, success: true,
message: 'Task approved', message: 'Task completed',
}, },
}; };
this.eventBus.emit(event); this.eventBus.emit(event);
// Also remove from blocked if it was there
this.blockedTasks.delete(taskId);
} }
/** /**
@@ -563,26 +501,4 @@ export class DefaultDispatchManager implements DispatchManager {
return { phases, tasks: implementationTasks, pages }; return { phases, tasks: implementationTasks, pages };
} }
/**
* Determine if a task requires approval before being marked complete.
* Checks task-level override first, then falls back to initiative setting.
*/
private async taskRequiresApproval(task: Task): Promise<boolean> {
// Task-level override takes precedence
if (task.requiresApproval !== null) {
return task.requiresApproval;
}
// Fall back to initiative setting if we have initiative access
if (this.initiativeRepository && task.initiativeId) {
const initiative = await this.initiativeRepository.findById(task.initiativeId);
if (initiative) {
return initiative.mergeRequiresApproval;
}
}
// If task has a phaseId but no initiativeId, we could traverse up but for now default to false
// Default: no approval required
return false;
}
} }

View File

@@ -49,7 +49,6 @@ function createMockDispatchManager(): DispatchManager {
getNextDispatchable: vi.fn().mockResolvedValue(null), getNextDispatchable: vi.fn().mockResolvedValue(null),
dispatchNext: vi.fn().mockResolvedValue({ success: false, reason: 'mock' }), dispatchNext: vi.fn().mockResolvedValue({ success: false, reason: 'mock' }),
completeTask: vi.fn(), completeTask: vi.fn(),
approveTask: vi.fn(),
blockTask: vi.fn(), blockTask: vi.fn(),
getQueueState: vi.fn().mockResolvedValue({ queued: [], ready: [], blocked: [] }), getQueueState: vi.fn().mockResolvedValue({ queued: [], ready: [], blocked: [] }),
}; };

View File

@@ -86,7 +86,6 @@ export interface DispatchManager {
/** /**
* Mark a task as complete. * Mark a task as complete.
* If the task requires approval, sets status to 'pending_approval' instead.
* Triggers re-evaluation of dependent tasks. * Triggers re-evaluation of dependent tasks.
* *
* @param taskId - ID of the completed task * @param taskId - ID of the completed task
@@ -94,14 +93,6 @@ export interface DispatchManager {
*/ */
completeTask(taskId: string, agentId?: string): Promise<void>; completeTask(taskId: string, agentId?: string): Promise<void>;
/**
* Approve a task that is pending approval.
* Sets status to 'completed' and emits completion event.
*
* @param taskId - ID of the task to approve
*/
approveTask(taskId: string): Promise<void>;
/** /**
* Mark a task as blocked. * Mark a task as blocked.
* Task will not be dispatched until unblocked. * Task will not be dispatched until unblocked.

View File

@@ -0,0 +1,6 @@
-- Migrate any pending_approval tasks to completed (unblock the pipeline)
UPDATE tasks SET status = 'completed' WHERE status = 'pending_approval';
-- Note: SQLite cannot drop columns. The merge_requires_approval column on
-- initiatives and requires_approval column on tasks are left in the DB but
-- removed from the Drizzle schema. Drizzle ignores extra DB columns.

View File

@@ -211,6 +211,13 @@
"when": 1772064000000, "when": 1772064000000,
"tag": "0029_add_project_last_fetched_at", "tag": "0029_add_project_last_fetched_at",
"breakpoints": true "breakpoints": true
},
{
"idx": 30,
"version": "6",
"when": 1772150400000,
"tag": "0030_remove_task_approval",
"breakpoints": true
} }
] ]
} }

View File

@@ -32,7 +32,6 @@ export type {
TaskDispatchedEvent, TaskDispatchedEvent,
TaskCompletedEvent, TaskCompletedEvent,
TaskBlockedEvent, TaskBlockedEvent,
TaskPendingApprovalEvent,
PhaseQueuedEvent, PhaseQueuedEvent,
PhaseStartedEvent, PhaseStartedEvent,
PhaseCompletedEvent, PhaseCompletedEvent,

View File

@@ -272,16 +272,6 @@ export interface TaskBlockedEvent extends DomainEvent {
}; };
} }
export interface TaskPendingApprovalEvent extends DomainEvent {
type: 'task:pending_approval';
payload: {
taskId: string;
agentId: string;
category: string;
name: string;
};
}
/** /**
* Phase Events * Phase Events
*/ */
@@ -647,7 +637,6 @@ export type DomainEventMap =
| TaskDispatchedEvent | TaskDispatchedEvent
| TaskCompletedEvent | TaskCompletedEvent
| TaskBlockedEvent | TaskBlockedEvent
| TaskPendingApprovalEvent
| PhaseQueuedEvent | PhaseQueuedEvent
| PhaseStartedEvent | PhaseStartedEvent
| PhaseCompletedEvent | PhaseCompletedEvent

View File

@@ -631,7 +631,6 @@ export function createTestHarness(): TestHarness {
description, description,
category: 'detail', category: 'detail',
type: 'auto', type: 'auto',
requiresApproval: true,
}); });
}, },

View File

@@ -69,7 +69,7 @@ export function printDetailResult(phase: Phase, tasks: Task[]): void {
console.log(`\n[DETAIL] Phase "${phase.name}" → ${tasks.length} task(s)`); console.log(`\n[DETAIL] Phase "${phase.name}" → ${tasks.length} task(s)`);
console.log(THIN); console.log(THIN);
tasks.forEach((t, i) => { tasks.forEach((t, i) => {
const flags = [t.category, t.type, t.requiresApproval ? 'approval-required' : 'auto'].join(', '); const flags = [t.category, t.type].join(', ');
line(`${i + 1}. ${t.name} [${flags}]`); line(`${i + 1}. ${t.name} [${flags}]`);
if (t.description) { if (t.description) {
line(` ${t.description.slice(0, 120)}`); line(` ${t.description.slice(0, 120)}`);

View File

@@ -204,7 +204,6 @@ export function initiativeProcedures(publicProcedure: ProcedureBuilder) {
updateInitiativeConfig: publicProcedure updateInitiativeConfig: publicProcedure
.input(z.object({ .input(z.object({
initiativeId: z.string().min(1), initiativeId: z.string().min(1),
mergeRequiresApproval: z.boolean().optional(),
executionMode: z.enum(['yolo', 'review_per_phase']).optional(), executionMode: z.enum(['yolo', 'review_per_phase']).optional(),
branch: z.string().nullable().optional(), branch: z.string().nullable().optional(),
})) }))

View File

@@ -38,7 +38,7 @@ export function taskProcedures(publicProcedure: ProcedureBuilder) {
updateTaskStatus: publicProcedure updateTaskStatus: publicProcedure
.input(z.object({ .input(z.object({
id: z.string().min(1), id: z.string().min(1),
status: z.enum(['pending_approval', 'pending', 'in_progress', 'completed', 'blocked']), status: z.enum(['pending', 'in_progress', 'completed', 'blocked']),
})) }))
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const taskRepository = requireTaskRepository(ctx); const taskRepository = requireTaskRepository(ctx);
@@ -59,7 +59,6 @@ export function taskProcedures(publicProcedure: ProcedureBuilder) {
description: z.string().optional(), description: z.string().optional(),
category: z.enum(['execute', 'research', 'discuss', 'plan', 'detail', 'refine', 'verify', 'merge', 'review']).optional(), category: z.enum(['execute', 'research', 'discuss', 'plan', 'detail', 'refine', 'verify', 'merge', 'review']).optional(),
type: z.enum(['auto', 'checkpoint:human-verify', 'checkpoint:decision', 'checkpoint:human-action']).optional(), type: z.enum(['auto', 'checkpoint:human-verify', 'checkpoint:decision', 'checkpoint:human-action']).optional(),
requiresApproval: z.boolean().nullable().optional(),
})) }))
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const taskRepository = requireTaskRepository(ctx); const taskRepository = requireTaskRepository(ctx);
@@ -79,7 +78,6 @@ export function taskProcedures(publicProcedure: ProcedureBuilder) {
description: input.description ?? null, description: input.description ?? null,
category: input.category ?? 'execute', category: input.category ?? 'execute',
type: input.type ?? 'auto', type: input.type ?? 'auto',
requiresApproval: input.requiresApproval ?? null,
status: 'pending', status: 'pending',
}); });
}), }),
@@ -91,7 +89,6 @@ export function taskProcedures(publicProcedure: ProcedureBuilder) {
description: z.string().optional(), description: z.string().optional(),
category: z.enum(['execute', 'research', 'discuss', 'plan', 'detail', 'refine', 'verify', 'merge', 'review']).optional(), category: z.enum(['execute', 'research', 'discuss', 'plan', 'detail', 'refine', 'verify', 'merge', 'review']).optional(),
type: z.enum(['auto', 'checkpoint:human-verify', 'checkpoint:decision', 'checkpoint:human-action']).optional(), type: z.enum(['auto', 'checkpoint:human-verify', 'checkpoint:decision', 'checkpoint:human-action']).optional(),
requiresApproval: z.boolean().nullable().optional(),
})) }))
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const taskRepository = requireTaskRepository(ctx); const taskRepository = requireTaskRepository(ctx);
@@ -111,22 +108,10 @@ export function taskProcedures(publicProcedure: ProcedureBuilder) {
description: input.description ?? null, description: input.description ?? null,
category: input.category ?? 'execute', category: input.category ?? 'execute',
type: input.type ?? 'auto', type: input.type ?? 'auto',
requiresApproval: input.requiresApproval ?? null,
status: 'pending', status: 'pending',
}); });
}), }),
listPendingApprovals: publicProcedure
.input(z.object({
initiativeId: z.string().optional(),
phaseId: z.string().optional(),
category: z.enum(['execute', 'research', 'discuss', 'plan', 'detail', 'refine', 'verify', 'merge', 'review']).optional(),
}).optional())
.query(async ({ ctx, input }) => {
const taskRepository = requireTaskRepository(ctx);
return taskRepository.findPendingApproval(input);
}),
listInitiativeTasks: publicProcedure listInitiativeTasks: publicProcedure
.input(z.object({ initiativeId: z.string().min(1) })) .input(z.object({ initiativeId: z.string().min(1) }))
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
@@ -196,12 +181,5 @@ export function taskProcedures(publicProcedure: ProcedureBuilder) {
return edges; return edges;
}), }),
approveTask: publicProcedure
.input(z.object({ taskId: z.string().min(1) }))
.mutation(async ({ ctx, input }) => {
const dispatchManager = requireDispatchManager(ctx);
await dispatchManager.approveTask(input.taskId);
return { success: true };
}),
}; };
} }

View File

@@ -51,7 +51,6 @@ export function CreateInitiativeDialog({
name: name.trim(), name: name.trim(),
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
mergeRequiresApproval: true,
branch: null, branch: null,
projects: [], projects: [],
activity: { state: 'idle' as const, phasesTotal: 0, phasesCompleted: 0 }, activity: { state: 'idle' as const, phasesTotal: 0, phasesCompleted: 0 },

View File

@@ -17,7 +17,6 @@ export interface SerializedInitiative {
id: string; id: string;
name: string; name: string;
status: "active" | "completed" | "archived"; status: "active" | "completed" | "archived";
mergeRequiresApproval: boolean;
branch: string | null; branch: string | null;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;

View File

@@ -39,7 +39,6 @@ export function mapEntityStatus(rawStatus: string): StatusVariant {
// Warning / needs attention // Warning / needs attention
case "waiting_for_input": case "waiting_for_input":
case "pending_approval":
case "pending_review": case "pending_review":
case "approved": case "approved":
case "exhausted": case "exhausted":

View File

@@ -15,8 +15,7 @@ export interface SerializedTask {
type: "auto" | "checkpoint:human-verify" | "checkpoint:decision" | "checkpoint:human-action"; type: "auto" | "checkpoint:human-verify" | "checkpoint:decision" | "checkpoint:human-action";
category: string; category: string;
priority: "low" | "medium" | "high"; priority: "low" | "medium" | "high";
status: "pending_approval" | "pending" | "in_progress" | "completed" | "blocked"; status: "pending" | "in_progress" | "completed" | "blocked";
requiresApproval: boolean | null;
order: number; order: number;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef, useMemo } from "react"; import { useCallback, useEffect, useRef, useMemo } from "react";
import { motion, AnimatePresence } from "motion/react"; import { motion, AnimatePresence } from "motion/react";
import { X, Trash2, MessageCircle, CheckCircle2 } from "lucide-react"; import { X, Trash2, MessageCircle } from "lucide-react";
import type { ChatTarget } from "@/components/chat/ChatSlideOver"; import type { ChatTarget } from "@/components/chat/ChatSlideOver";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -20,7 +20,6 @@ interface TaskSlideOverProps {
export function TaskSlideOver({ onOpenChat }: TaskSlideOverProps) { export function TaskSlideOver({ onOpenChat }: TaskSlideOverProps) {
const { selectedEntry, setSelectedTaskId } = useExecutionContext(); const { selectedEntry, setSelectedTaskId } = useExecutionContext();
const queueTaskMutation = trpc.queueTask.useMutation(); const queueTaskMutation = trpc.queueTask.useMutation();
const approveTaskMutation = trpc.approveTask.useMutation();
const deleteTaskMutation = trpc.deleteTask.useMutation(); const deleteTaskMutation = trpc.deleteTask.useMutation();
const updateTaskMutation = trpc.updateTask.useMutation(); const updateTaskMutation = trpc.updateTask.useMutation();
@@ -230,32 +229,17 @@ export function TaskSlideOver({ onOpenChat }: TaskSlideOverProps) {
{/* Footer */} {/* Footer */}
<div className="flex items-center gap-2 border-t border-border px-5 py-3"> <div className="flex items-center gap-2 border-t border-border px-5 py-3">
{task.status === "pending_approval" ? ( <Button
<Button variant="outline"
size="sm" size="sm"
className="gap-1.5" disabled={!canQueue}
disabled={approveTaskMutation.isPending} onClick={() => {
onClick={() => { queueTaskMutation.mutate({ taskId: task.id });
approveTaskMutation.mutate({ taskId: task.id }); close();
close(); }}
}} >
> Queue Task
<CheckCircle2 className="h-3.5 w-3.5" /> </Button>
Approve
</Button>
) : (
<Button
variant="outline"
size="sm"
disabled={!canQueue}
onClick={() => {
queueTaskMutation.mutate({ taskId: task.id });
close();
}}
>
Queue Task
</Button>
)}
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"

View File

@@ -64,7 +64,6 @@ const INVALIDATION_MAP: Partial<Record<MutationName, QueryName[]>> = {
createInitiativeTask: ["listTasks", "listInitiativeTasks"], createInitiativeTask: ["listTasks", "listInitiativeTasks"],
createChildTasks: ["listTasks", "listInitiativeTasks", "listPhaseTasks"], createChildTasks: ["listTasks", "listInitiativeTasks", "listPhaseTasks"],
queueTask: ["listTasks", "listInitiativeTasks", "listPhaseTasks"], queueTask: ["listTasks", "listInitiativeTasks", "listPhaseTasks"],
approveTask: ["listTasks", "listInitiativeTasks", "listPhaseTasks", "listPendingApprovals"],
// --- Change Sets --- // --- Change Sets ---
revertChangeSet: ["listPhases", "listPhaseTasks", "listInitiativeTasks", "listPages", "getPage", "listChangeSets", "getRootPage"], revertChangeSet: ["listPhases", "listPhaseTasks", "listInitiativeTasks", "listPages", "getPage", "listChangeSets", "getRootPage"],

View File

@@ -19,7 +19,6 @@ All adapters use nanoid() for IDs, auto-manage timestamps, and use Drizzle's `.r
| id | text PK | nanoid | | id | text PK | nanoid |
| name | text NOT NULL | | | name | text NOT NULL | |
| status | text enum | 'active' \| 'pending_review' \| 'completed' \| 'archived', default 'active' | | status | text enum | 'active' \| 'pending_review' \| 'completed' \| 'archived', default 'active' |
| mergeRequiresApproval | integer/boolean | default true |
| branch | text nullable | auto-generated initiative branch (e.g., 'cw/user-auth') | | branch | text nullable | auto-generated initiative branch (e.g., 'cw/user-auth') |
| createdAt, updatedAt | integer/timestamp | | | createdAt, updatedAt | integer/timestamp | |
@@ -48,8 +47,7 @@ All adapters use nanoid() for IDs, auto-manage timestamps, and use Drizzle's `.r
| type | text enum | 'auto' \| 'checkpoint:human-verify' \| 'checkpoint:decision' \| 'checkpoint:human-action' | | type | text enum | 'auto' \| 'checkpoint:human-verify' \| 'checkpoint:decision' \| 'checkpoint:human-action' |
| category | text enum | 'execute' \| 'research' \| 'discuss' \| 'plan' \| 'detail' \| 'refine' \| 'verify' \| 'merge' \| 'review' | | category | text enum | 'execute' \| 'research' \| 'discuss' \| 'plan' \| 'detail' \| 'refine' \| 'verify' \| 'merge' \| 'review' |
| priority | text enum | 'low' \| 'medium' \| 'high' | | priority | text enum | 'low' \| 'medium' \| 'high' |
| status | text enum | 'pending_approval' \| 'pending' \| 'in_progress' \| 'completed' \| 'blocked' | | status | text enum | 'pending' \| 'in_progress' \| 'completed' \| 'blocked' |
| requiresApproval | integer/boolean nullable | null = inherit from initiative |
| order | integer | default 0 | | order | integer | default 0 |
| summary | text nullable | Agent result summary — propagated to dependent tasks as context | | summary | text nullable | Agent result summary — propagated to dependent tasks as context |
| createdAt, updatedAt | integer/timestamp | | | createdAt, updatedAt | integer/timestamp | |
@@ -222,7 +220,7 @@ Index: `(phaseId)`.
|-----------|-------------| |-----------|-------------|
| InitiativeRepository | create, findById, findAll, findByStatus, update, delete | | InitiativeRepository | create, findById, findAll, findByStatus, update, delete |
| PhaseRepository | + createDependency, getDependencies, getDependents, findByInitiativeId | | PhaseRepository | + createDependency, getDependencies, getDependents, findByInitiativeId |
| TaskRepository | + findByParentTaskId, findByPhaseId, findPendingApproval, createDependency | | TaskRepository | + findByParentTaskId, findByPhaseId, createDependency |
| AgentRepository | + findByName, findByTaskId, findBySessionId, findByStatus | | AgentRepository | + findByName, findByTaskId, findBySessionId, findByStatus |
| MessageRepository | + findPendingForUser, findRequiringResponse, findReplies | | MessageRepository | + findPendingForUser, findRequiringResponse, findReplies |
| PageRepository | + findRootPage, getOrCreateRootPage, findByParentPageId | | PageRepository | + findRootPage, getOrCreateRootPage, findByParentPageId |
@@ -244,4 +242,4 @@ Key rules:
- See [database-migrations.md](database-migrations.md) for full workflow - See [database-migrations.md](database-migrations.md) for full workflow
- Snapshots stale after 0008; migrations 0008+ are hand-written - Snapshots stale after 0008; migrations 0008+ are hand-written
Current migrations: 0000 through 0028 (29 total). Current migrations: 0000 through 0030 (31 total).

View File

@@ -16,7 +16,7 @@
| Category | Events | Count | | Category | Events | Count |
|----------|--------|-------| |----------|--------|-------|
| **Agent** | `agent:spawned`, `agent:stopped`, `agent:crashed`, `agent:resumed`, `agent:account_switched`, `agent:deleted`, `agent:waiting`, `agent:output` | 8 | | **Agent** | `agent:spawned`, `agent:stopped`, `agent:crashed`, `agent:resumed`, `agent:account_switched`, `agent:deleted`, `agent:waiting`, `agent:output` | 8 |
| **Task** | `task:queued`, `task:dispatched`, `task:completed`, `task:blocked`, `task:pending_approval` | 5 | | **Task** | `task:queued`, `task:dispatched`, `task:completed`, `task:blocked` | 4 |
| **Phase** | `phase:queued`, `phase:started`, `phase:completed`, `phase:blocked`, `phase:changes_requested` | 5 | | **Phase** | `phase:queued`, `phase:started`, `phase:completed`, `phase:blocked`, `phase:changes_requested` | 5 |
| **Merge** | `merge:queued`, `merge:started`, `merge:completed`, `merge:conflicted` | 4 | | **Merge** | `merge:queued`, `merge:started`, `merge:completed`, `merge:conflicted` | 4 |
| **Page** | `page:created`, `page:updated`, `page:deleted` | 3 | | **Page** | `page:created`, `page:updated`, `page:deleted` | 3 |
@@ -67,9 +67,7 @@ InitiativeReviewApprovedEvent { initiativeId, branch, strategy: 'push_branch' |
5. **Checkpoint skip** — Tasks with type starting with `checkpoint:` skip auto-dispatch 5. **Checkpoint skip** — Tasks with type starting with `checkpoint:` skip auto-dispatch
6. **Planning skip** — Planning-category tasks (research, discuss, plan, detail, refine) skip auto-dispatch — they use the architect flow 6. **Planning skip** — Planning-category tasks (research, discuss, plan, detail, refine) skip auto-dispatch — they use the architect flow
7. **Summary propagation**`completeTask()` reads the completing agent's `result.message` and stores it on the task's `summary` column. Dependent tasks see this summary in `context/tasks/<id>.md` frontmatter. 7. **Summary propagation**`completeTask()` reads the completing agent's `result.message` and stores it on the task's `summary` column. Dependent tasks see this summary in `context/tasks/<id>.md` frontmatter.
8. **Approval check**`completeTask()` checks `requiresApproval` (task-level, then initiative-level) 8. **Spawn failure** — If `agentManager.spawn()` throws, the task is blocked via `blockTask()` with the error message. The dispatch cycle continues instead of crashing.
9. **Approval flow** — If approval required: status → `pending_approval`, emit `task:pending_approval`
10. **Spawn failure** — If `agentManager.spawn()` throws, the task is blocked via `blockTask()` with the error message. The dispatch cycle continues instead of crashing.
### DispatchManager Methods ### DispatchManager Methods
@@ -78,8 +76,7 @@ InitiativeReviewApprovedEvent { initiativeId, branch, strategy: 'push_branch' |
| `queue(taskId)` | Add task to dispatch queue | | `queue(taskId)` | Add task to dispatch queue |
| `dispatchNext()` | Find and dispatch next ready task | | `dispatchNext()` | Find and dispatch next ready task |
| `getNextDispatchable()` | Get next task without dispatching | | `getNextDispatchable()` | Get next task without dispatching |
| `completeTask(taskId, agentId?)` | Complete with approval check | | `completeTask(taskId, agentId?)` | Complete task |
| `approveTask(taskId)` | Approve pending task |
| `blockTask(taskId, reason)` | Block task with reason | | `blockTask(taskId, reason)` | Block task with reason |
| `getQueueState()` | Return queued, ready, blocked tasks | | `getQueueState()` | Return queued, ready, blocked tasks |

View File

@@ -78,11 +78,9 @@ Each procedure uses `require*Repository(ctx)` helpers that throw `TRPCError(INTE
| createPhaseTask | mutation | Create task on phase | | createPhaseTask | mutation | Create task on phase |
| listInitiativeTasks | query | All tasks for initiative | | listInitiativeTasks | query | All tasks for initiative |
| listPhaseTasks | query | All tasks for phase | | listPhaseTasks | query | All tasks for phase |
| listPendingApprovals | query | Tasks with status=pending_approval |
| deleteTask | mutation | Delete a task by ID | | deleteTask | mutation | Delete a task by ID |
| listPhaseTaskDependencies | query | All task dependency edges for tasks in a phase | | listPhaseTaskDependencies | query | All task dependency edges for tasks in a phase |
| listInitiativeTaskDependencies | query | All task dependency edges for tasks in an initiative | | listInitiativeTaskDependencies | query | All task dependency edges for tasks in an initiative |
| approveTask | mutation | Approve and complete task |
### Initiatives ### Initiatives
| Procedure | Type | Description | | Procedure | Type | Description |
@@ -92,7 +90,7 @@ Each procedure uses `require*Repository(ctx)` helpers that throw `TRPCError(INTE
| getInitiative | query | With projects array | | getInitiative | query | With projects array |
| updateInitiative | mutation | Name, status | | updateInitiative | mutation | Name, status |
| deleteInitiative | mutation | Cascade delete initiative and all children | | deleteInitiative | mutation | Cascade delete initiative and all children |
| updateInitiativeConfig | mutation | mergeRequiresApproval, executionMode, branch | | updateInitiativeConfig | mutation | executionMode, branch |
| getInitiativeReviewDiff | query | Full diff of initiative branch vs project default branch | | getInitiativeReviewDiff | query | Full diff of initiative branch vs project default branch |
| getInitiativeReviewCommits | query | Commits on initiative branch not on default branch | | getInitiativeReviewCommits | query | Commits on initiative branch not on default branch |
| getInitiativeCommitDiff | query | Single commit diff for initiative review | | getInitiativeCommitDiff | query | Single commit diff for initiative review |
@@ -146,7 +144,7 @@ Each procedure uses `require*Repository(ctx)` helpers that throw `TRPCError(INTE
| queueTask | mutation | Add task to dispatch queue | | queueTask | mutation | Add task to dispatch queue |
| dispatchNext | mutation | Dispatch next ready task | | dispatchNext | mutation | Dispatch next ready task |
| getQueueState | query | Queue state | | getQueueState | query | Queue state |
| completeTask | mutation | Complete with approval check | | completeTask | mutation | Complete task |
### Coordination (Merge Queue) ### Coordination (Merge Queue)
| Procedure | Type | Description | | Procedure | Type | Description |