diff --git a/src/db.test.ts b/src/db.test.ts index 610f708..81012c4 100644 --- a/src/db.test.ts +++ b/src/db.test.ts @@ -561,6 +561,7 @@ describe('paired task state', () => { status: 'active', arbiter_verdict: null, arbiter_requested_at: null, + completion_reason: null, created_at: '2026-03-28T00:00:00.000Z', updated_at: '2026-03-28T00:00:00.000Z', }); @@ -602,6 +603,7 @@ describe('paired task state', () => { status: 'active', arbiter_verdict: null, arbiter_requested_at: null, + completion_reason: null, created_at: '2026-03-28T00:00:00.000Z', updated_at: '2026-03-28T00:00:00.000Z', }); diff --git a/src/db.ts b/src/db.ts index 4ec0589..55e1ffe 100644 --- a/src/db.ts +++ b/src/db.ts @@ -256,6 +256,7 @@ function createSchema(database: Database): void { status TEXT NOT NULL DEFAULT 'active', arbiter_verdict TEXT, arbiter_requested_at TEXT, + completion_reason TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, CHECK (status IN ('active', 'review_ready', 'in_review', 'merge_ready', 'completed', 'arbiter_requested', 'in_arbitration')) @@ -633,6 +634,7 @@ function createSchema(database: Database): void { status TEXT NOT NULL DEFAULT 'active', arbiter_verdict TEXT, arbiter_requested_at TEXT, + completion_reason TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, CHECK (status IN ('active', 'review_ready', 'in_review', 'merge_ready', 'completed', 'arbiter_requested', 'in_arbitration')) @@ -664,6 +666,15 @@ function createSchema(database: Database): void { /* column already exists */ } + // Migration: add completion_reason column to paired_tasks if it doesn't exist + try { + database.exec( + `ALTER TABLE paired_tasks ADD COLUMN completion_reason TEXT`, + ); + } catch { + /* column already exists */ + } + // Drop legacy tables that are no longer used for (const table of [ 'paired_executions', @@ -2035,6 +2046,7 @@ export function updatePairedTask( | 'status' | 'arbiter_verdict' | 'arbiter_requested_at' + | 'completion_reason' | 'updated_at' > >, @@ -2074,6 +2086,10 @@ export function updatePairedTask( fields.push('arbiter_requested_at = ?'); values.push(updates.arbiter_requested_at); } + if (updates.completion_reason !== undefined) { + fields.push('completion_reason = ?'); + values.push(updates.completion_reason); + } if (updates.updated_at !== undefined) { fields.push('updated_at = ?'); values.push(updates.updated_at); diff --git a/src/message-agent-executor.test.ts b/src/message-agent-executor.test.ts index 3858412..6908b4e 100644 --- a/src/message-agent-executor.test.ts +++ b/src/message-agent-executor.test.ts @@ -361,6 +361,7 @@ describe('runAgentForGroup room memory', () => { status: 'active', arbiter_verdict: null, arbiter_requested_at: null, + completion_reason: null, created_at: '2026-03-29T00:00:00.000Z', updated_at: '2026-03-29T00:00:00.000Z', }, @@ -425,6 +426,7 @@ describe('runAgentForGroup room memory', () => { status: 'active', arbiter_verdict: null, arbiter_requested_at: null, + completion_reason: null, created_at: '2026-03-28T00:00:00.000Z', updated_at: '2026-03-28T00:00:00.000Z', }, @@ -514,6 +516,7 @@ describe('runAgentForGroup room memory', () => { status: 'active', arbiter_verdict: null, arbiter_requested_at: null, + completion_reason: null, created_at: '2026-03-28T00:00:00.000Z', updated_at: '2026-03-28T00:00:00.000Z', }, diff --git a/src/message-runtime.test.ts b/src/message-runtime.test.ts index e1ca1f9..db333d2 100644 --- a/src/message-runtime.test.ts +++ b/src/message-runtime.test.ts @@ -565,6 +565,7 @@ describe('createMessageRuntime', () => { status: 'review_ready', arbiter_verdict: null, arbiter_requested_at: null, + completion_reason: null, created_at: '2026-03-30T00:00:00.000Z', updated_at: '2026-03-30T00:00:00.000Z', }); diff --git a/src/message-runtime.ts b/src/message-runtime.ts index 1e6008e..377db25 100644 --- a/src/message-runtime.ts +++ b/src/message-runtime.ts @@ -846,6 +846,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): { if (task) { updatePairedTask(task.id, { status: 'completed', + completion_reason: 'stopped', updated_at: new Date().toISOString(), }); } diff --git a/src/paired-execution-context.test.ts b/src/paired-execution-context.test.ts index 9df010f..d1e07fa 100644 --- a/src/paired-execution-context.test.ts +++ b/src/paired-execution-context.test.ts @@ -112,6 +112,7 @@ function buildPairedTask(overrides: Partial = {}): PairedTask { status: 'active', arbiter_verdict: null, arbiter_requested_at: null, + completion_reason: null, created_at: '2026-03-28T00:00:00.000Z', updated_at: '2026-03-28T00:00:00.000Z', ...overrides, @@ -202,6 +203,7 @@ describe('paired execution context', () => { status: 'review_ready', arbiter_verdict: null, arbiter_requested_at: null, + completion_reason: null, created_at: '2026-03-28T00:00:00.000Z', updated_at: '2026-03-28T00:00:00.000Z', }); @@ -236,6 +238,7 @@ describe('paired execution context', () => { status: 'review_ready', arbiter_verdict: null, arbiter_requested_at: null, + completion_reason: null, created_at: '2026-03-28T00:00:00.000Z', updated_at: '2026-03-28T00:00:00.000Z', }); @@ -273,6 +276,7 @@ describe('paired execution context', () => { status: 'active', arbiter_verdict: null, arbiter_requested_at: null, + completion_reason: null, created_at: '2026-03-28T00:00:00.000Z', updated_at: '2026-03-28T00:00:00.000Z', }); @@ -316,6 +320,7 @@ describe('paired execution context', () => { status: 'in_review', arbiter_verdict: null, arbiter_requested_at: null, + completion_reason: null, created_at: '2026-03-28T00:00:00.000Z', updated_at: '2026-03-28T00:00:00.000Z', }); diff --git a/src/paired-execution-context.ts b/src/paired-execution-context.ts index 8901abb..cd4af75 100644 --- a/src/paired-execution-context.ts +++ b/src/paired-execution-context.ts @@ -189,6 +189,7 @@ function ensureActiveTask( status: 'active', arbiter_verdict: null, arbiter_requested_at: null, + completion_reason: null, created_at: now, updated_at: now, }; @@ -384,6 +385,7 @@ export function completePairedExecutionContext(args: { updatePairedTask(taskId, { status: verdict === 'done' ? 'merge_ready' : 'completed', ...(verdict === 'done' ? { source_ref: approvedSourceRef } : {}), + ...(verdict !== 'done' ? { completion_reason: 'escalated' } : {}), updated_at: now, }); logger.info( @@ -442,7 +444,7 @@ export function completePairedExecutionContext(args: { if (hasNewChanges === false) { // No code changes since reviewer approved → finalize complete - updatePairedTask(taskId, { status: 'completed', updated_at: now }); + updatePairedTask(taskId, { status: 'completed', completion_reason: 'done', updated_at: now }); logger.info( { taskId, summary: args.summary?.slice(0, 100) }, 'Owner finalized after reviewer approval — task completed', @@ -573,7 +575,7 @@ export function completePairedExecutionContext(args: { 'Reviewer blocked/needs_context — requesting arbiter before escalating', ); } else { - updatePairedTask(taskId, { status: 'completed', updated_at: now }); + updatePairedTask(taskId, { status: 'completed', completion_reason: 'escalated', updated_at: now }); logger.info( { taskId, verdict, summary: args.summary?.slice(0, 100) }, 'Reviewer escalated to user — ping-pong stopped', @@ -598,7 +600,7 @@ export function completePairedExecutionContext(args: { 'Deadlock detected — requesting arbiter intervention', ); } else { - updatePairedTask(taskId, { status: 'completed', updated_at: now }); + updatePairedTask(taskId, { status: 'completed', completion_reason: 'escalated', updated_at: now }); logger.info( { taskId, verdict, roundTrips: task.round_trip_count }, 'Stopped ping-pong — escalating to user (arbiter not configured)', @@ -646,6 +648,7 @@ export function completePairedExecutionContext(args: { updatePairedTask(taskId, { status: 'completed', arbiter_verdict: 'escalate', + completion_reason: 'arbiter_escalated', updated_at: now, }); logger.info({ taskId }, 'Arbiter escalated to user — task completed'); @@ -655,6 +658,7 @@ export function completePairedExecutionContext(args: { updatePairedTask(taskId, { status: 'completed', arbiter_verdict: 'unknown', + completion_reason: 'arbiter_escalated', updated_at: now, }); break; diff --git a/src/paired-workspace-manager.test.ts b/src/paired-workspace-manager.test.ts index e92f7b7..feaa1cc 100644 --- a/src/paired-workspace-manager.test.ts +++ b/src/paired-workspace-manager.test.ts @@ -80,6 +80,7 @@ describe('paired workspace manager', () => { status: 'active', arbiter_verdict: null, arbiter_requested_at: null, + completion_reason: null, created_at: now, updated_at: now, }); @@ -171,6 +172,7 @@ describe('paired workspace manager', () => { status: 'active', arbiter_verdict: null, arbiter_requested_at: null, + completion_reason: null, created_at: now, updated_at: now, }); @@ -249,6 +251,7 @@ describe('paired workspace manager', () => { status: 'active', arbiter_verdict: null, arbiter_requested_at: null, + completion_reason: null, created_at: now, updated_at: now, }); @@ -326,6 +329,7 @@ describe('paired workspace manager', () => { status: 'active', arbiter_verdict: null, arbiter_requested_at: null, + completion_reason: null, created_at: now, updated_at: now, }); @@ -469,6 +473,7 @@ describe('paired workspace manager', () => { status: 'active', arbiter_verdict: null, arbiter_requested_at: null, + completion_reason: null, created_at: now, updated_at: now, }); @@ -533,6 +538,7 @@ describe('paired workspace manager', () => { status: 'review_ready', arbiter_verdict: null, arbiter_requested_at: null, + completion_reason: null, created_at: now, updated_at: '2026-03-28T00:01:00.000Z', }); @@ -601,6 +607,7 @@ describe('paired workspace manager', () => { status: 'active', arbiter_verdict: null, arbiter_requested_at: null, + completion_reason: null, created_at: now, updated_at: now, }); diff --git a/src/types.ts b/src/types.ts index 2347955..e2c392c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -71,6 +71,7 @@ export interface PairedTask { status: PairedTaskStatus; arbiter_verdict: string | null; arbiter_requested_at: string | null; + completion_reason: string | null; created_at: string; updated_at: string; }