feat: add completion_reason to paired_tasks (done/escalated/stopped/arbiter_escalated)
- owner can summon arbiter via BLOCKED/NEEDS_CONTEXT verdict - completion_reason tracks why task ended: done (success), escalated (reviewer gave up), stopped (/stop), arbiter_escalated
This commit is contained in:
@@ -561,6 +561,7 @@ describe('paired task state', () => {
|
|||||||
status: 'active',
|
status: 'active',
|
||||||
arbiter_verdict: null,
|
arbiter_verdict: null,
|
||||||
arbiter_requested_at: null,
|
arbiter_requested_at: null,
|
||||||
|
completion_reason: null,
|
||||||
created_at: '2026-03-28T00:00:00.000Z',
|
created_at: '2026-03-28T00:00:00.000Z',
|
||||||
updated_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',
|
status: 'active',
|
||||||
arbiter_verdict: null,
|
arbiter_verdict: null,
|
||||||
arbiter_requested_at: null,
|
arbiter_requested_at: null,
|
||||||
|
completion_reason: null,
|
||||||
created_at: '2026-03-28T00:00:00.000Z',
|
created_at: '2026-03-28T00:00:00.000Z',
|
||||||
updated_at: '2026-03-28T00:00:00.000Z',
|
updated_at: '2026-03-28T00:00:00.000Z',
|
||||||
});
|
});
|
||||||
|
|||||||
16
src/db.ts
16
src/db.ts
@@ -256,6 +256,7 @@ function createSchema(database: Database): void {
|
|||||||
status TEXT NOT NULL DEFAULT 'active',
|
status TEXT NOT NULL DEFAULT 'active',
|
||||||
arbiter_verdict TEXT,
|
arbiter_verdict TEXT,
|
||||||
arbiter_requested_at TEXT,
|
arbiter_requested_at TEXT,
|
||||||
|
completion_reason TEXT,
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
updated_at TEXT NOT NULL,
|
updated_at TEXT NOT NULL,
|
||||||
CHECK (status IN ('active', 'review_ready', 'in_review', 'merge_ready', 'completed', 'arbiter_requested', 'in_arbitration'))
|
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',
|
status TEXT NOT NULL DEFAULT 'active',
|
||||||
arbiter_verdict TEXT,
|
arbiter_verdict TEXT,
|
||||||
arbiter_requested_at TEXT,
|
arbiter_requested_at TEXT,
|
||||||
|
completion_reason TEXT,
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
updated_at TEXT NOT NULL,
|
updated_at TEXT NOT NULL,
|
||||||
CHECK (status IN ('active', 'review_ready', 'in_review', 'merge_ready', 'completed', 'arbiter_requested', 'in_arbitration'))
|
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 */
|
/* 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
|
// Drop legacy tables that are no longer used
|
||||||
for (const table of [
|
for (const table of [
|
||||||
'paired_executions',
|
'paired_executions',
|
||||||
@@ -2035,6 +2046,7 @@ export function updatePairedTask(
|
|||||||
| 'status'
|
| 'status'
|
||||||
| 'arbiter_verdict'
|
| 'arbiter_verdict'
|
||||||
| 'arbiter_requested_at'
|
| 'arbiter_requested_at'
|
||||||
|
| 'completion_reason'
|
||||||
| 'updated_at'
|
| 'updated_at'
|
||||||
>
|
>
|
||||||
>,
|
>,
|
||||||
@@ -2074,6 +2086,10 @@ export function updatePairedTask(
|
|||||||
fields.push('arbiter_requested_at = ?');
|
fields.push('arbiter_requested_at = ?');
|
||||||
values.push(updates.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) {
|
if (updates.updated_at !== undefined) {
|
||||||
fields.push('updated_at = ?');
|
fields.push('updated_at = ?');
|
||||||
values.push(updates.updated_at);
|
values.push(updates.updated_at);
|
||||||
|
|||||||
@@ -361,6 +361,7 @@ describe('runAgentForGroup room memory', () => {
|
|||||||
status: 'active',
|
status: 'active',
|
||||||
arbiter_verdict: null,
|
arbiter_verdict: null,
|
||||||
arbiter_requested_at: null,
|
arbiter_requested_at: null,
|
||||||
|
completion_reason: null,
|
||||||
created_at: '2026-03-29T00:00:00.000Z',
|
created_at: '2026-03-29T00:00:00.000Z',
|
||||||
updated_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',
|
status: 'active',
|
||||||
arbiter_verdict: null,
|
arbiter_verdict: null,
|
||||||
arbiter_requested_at: null,
|
arbiter_requested_at: null,
|
||||||
|
completion_reason: null,
|
||||||
created_at: '2026-03-28T00:00:00.000Z',
|
created_at: '2026-03-28T00:00:00.000Z',
|
||||||
updated_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',
|
status: 'active',
|
||||||
arbiter_verdict: null,
|
arbiter_verdict: null,
|
||||||
arbiter_requested_at: null,
|
arbiter_requested_at: null,
|
||||||
|
completion_reason: null,
|
||||||
created_at: '2026-03-28T00:00:00.000Z',
|
created_at: '2026-03-28T00:00:00.000Z',
|
||||||
updated_at: '2026-03-28T00:00:00.000Z',
|
updated_at: '2026-03-28T00:00:00.000Z',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -565,6 +565,7 @@ describe('createMessageRuntime', () => {
|
|||||||
status: 'review_ready',
|
status: 'review_ready',
|
||||||
arbiter_verdict: null,
|
arbiter_verdict: null,
|
||||||
arbiter_requested_at: null,
|
arbiter_requested_at: null,
|
||||||
|
completion_reason: null,
|
||||||
created_at: '2026-03-30T00:00:00.000Z',
|
created_at: '2026-03-30T00:00:00.000Z',
|
||||||
updated_at: '2026-03-30T00:00:00.000Z',
|
updated_at: '2026-03-30T00:00:00.000Z',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -846,6 +846,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
if (task) {
|
if (task) {
|
||||||
updatePairedTask(task.id, {
|
updatePairedTask(task.id, {
|
||||||
status: 'completed',
|
status: 'completed',
|
||||||
|
completion_reason: 'stopped',
|
||||||
updated_at: new Date().toISOString(),
|
updated_at: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ function buildPairedTask(overrides: Partial<PairedTask> = {}): PairedTask {
|
|||||||
status: 'active',
|
status: 'active',
|
||||||
arbiter_verdict: null,
|
arbiter_verdict: null,
|
||||||
arbiter_requested_at: null,
|
arbiter_requested_at: null,
|
||||||
|
completion_reason: null,
|
||||||
created_at: '2026-03-28T00:00:00.000Z',
|
created_at: '2026-03-28T00:00:00.000Z',
|
||||||
updated_at: '2026-03-28T00:00:00.000Z',
|
updated_at: '2026-03-28T00:00:00.000Z',
|
||||||
...overrides,
|
...overrides,
|
||||||
@@ -202,6 +203,7 @@ describe('paired execution context', () => {
|
|||||||
status: 'review_ready',
|
status: 'review_ready',
|
||||||
arbiter_verdict: null,
|
arbiter_verdict: null,
|
||||||
arbiter_requested_at: null,
|
arbiter_requested_at: null,
|
||||||
|
completion_reason: null,
|
||||||
created_at: '2026-03-28T00:00:00.000Z',
|
created_at: '2026-03-28T00:00:00.000Z',
|
||||||
updated_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',
|
status: 'review_ready',
|
||||||
arbiter_verdict: null,
|
arbiter_verdict: null,
|
||||||
arbiter_requested_at: null,
|
arbiter_requested_at: null,
|
||||||
|
completion_reason: null,
|
||||||
created_at: '2026-03-28T00:00:00.000Z',
|
created_at: '2026-03-28T00:00:00.000Z',
|
||||||
updated_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',
|
status: 'active',
|
||||||
arbiter_verdict: null,
|
arbiter_verdict: null,
|
||||||
arbiter_requested_at: null,
|
arbiter_requested_at: null,
|
||||||
|
completion_reason: null,
|
||||||
created_at: '2026-03-28T00:00:00.000Z',
|
created_at: '2026-03-28T00:00:00.000Z',
|
||||||
updated_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',
|
status: 'in_review',
|
||||||
arbiter_verdict: null,
|
arbiter_verdict: null,
|
||||||
arbiter_requested_at: null,
|
arbiter_requested_at: null,
|
||||||
|
completion_reason: null,
|
||||||
created_at: '2026-03-28T00:00:00.000Z',
|
created_at: '2026-03-28T00:00:00.000Z',
|
||||||
updated_at: '2026-03-28T00:00:00.000Z',
|
updated_at: '2026-03-28T00:00:00.000Z',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ function ensureActiveTask(
|
|||||||
status: 'active',
|
status: 'active',
|
||||||
arbiter_verdict: null,
|
arbiter_verdict: null,
|
||||||
arbiter_requested_at: null,
|
arbiter_requested_at: null,
|
||||||
|
completion_reason: null,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
};
|
};
|
||||||
@@ -384,6 +385,7 @@ export function completePairedExecutionContext(args: {
|
|||||||
updatePairedTask(taskId, {
|
updatePairedTask(taskId, {
|
||||||
status: verdict === 'done' ? 'merge_ready' : 'completed',
|
status: verdict === 'done' ? 'merge_ready' : 'completed',
|
||||||
...(verdict === 'done' ? { source_ref: approvedSourceRef } : {}),
|
...(verdict === 'done' ? { source_ref: approvedSourceRef } : {}),
|
||||||
|
...(verdict !== 'done' ? { completion_reason: 'escalated' } : {}),
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
});
|
});
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -442,7 +444,7 @@ export function completePairedExecutionContext(args: {
|
|||||||
|
|
||||||
if (hasNewChanges === false) {
|
if (hasNewChanges === false) {
|
||||||
// No code changes since reviewer approved → finalize complete
|
// 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(
|
logger.info(
|
||||||
{ taskId, summary: args.summary?.slice(0, 100) },
|
{ taskId, summary: args.summary?.slice(0, 100) },
|
||||||
'Owner finalized after reviewer approval — task completed',
|
'Owner finalized after reviewer approval — task completed',
|
||||||
@@ -573,7 +575,7 @@ export function completePairedExecutionContext(args: {
|
|||||||
'Reviewer blocked/needs_context — requesting arbiter before escalating',
|
'Reviewer blocked/needs_context — requesting arbiter before escalating',
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
updatePairedTask(taskId, { status: 'completed', updated_at: now });
|
updatePairedTask(taskId, { status: 'completed', completion_reason: 'escalated', updated_at: now });
|
||||||
logger.info(
|
logger.info(
|
||||||
{ taskId, verdict, summary: args.summary?.slice(0, 100) },
|
{ taskId, verdict, summary: args.summary?.slice(0, 100) },
|
||||||
'Reviewer escalated to user — ping-pong stopped',
|
'Reviewer escalated to user — ping-pong stopped',
|
||||||
@@ -598,7 +600,7 @@ export function completePairedExecutionContext(args: {
|
|||||||
'Deadlock detected — requesting arbiter intervention',
|
'Deadlock detected — requesting arbiter intervention',
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
updatePairedTask(taskId, { status: 'completed', updated_at: now });
|
updatePairedTask(taskId, { status: 'completed', completion_reason: 'escalated', updated_at: now });
|
||||||
logger.info(
|
logger.info(
|
||||||
{ taskId, verdict, roundTrips: task.round_trip_count },
|
{ taskId, verdict, roundTrips: task.round_trip_count },
|
||||||
'Stopped ping-pong — escalating to user (arbiter not configured)',
|
'Stopped ping-pong — escalating to user (arbiter not configured)',
|
||||||
@@ -646,6 +648,7 @@ export function completePairedExecutionContext(args: {
|
|||||||
updatePairedTask(taskId, {
|
updatePairedTask(taskId, {
|
||||||
status: 'completed',
|
status: 'completed',
|
||||||
arbiter_verdict: 'escalate',
|
arbiter_verdict: 'escalate',
|
||||||
|
completion_reason: 'arbiter_escalated',
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
});
|
});
|
||||||
logger.info({ taskId }, 'Arbiter escalated to user — task completed');
|
logger.info({ taskId }, 'Arbiter escalated to user — task completed');
|
||||||
@@ -655,6 +658,7 @@ export function completePairedExecutionContext(args: {
|
|||||||
updatePairedTask(taskId, {
|
updatePairedTask(taskId, {
|
||||||
status: 'completed',
|
status: 'completed',
|
||||||
arbiter_verdict: 'unknown',
|
arbiter_verdict: 'unknown',
|
||||||
|
completion_reason: 'arbiter_escalated',
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ describe('paired workspace manager', () => {
|
|||||||
status: 'active',
|
status: 'active',
|
||||||
arbiter_verdict: null,
|
arbiter_verdict: null,
|
||||||
arbiter_requested_at: null,
|
arbiter_requested_at: null,
|
||||||
|
completion_reason: null,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
});
|
});
|
||||||
@@ -171,6 +172,7 @@ describe('paired workspace manager', () => {
|
|||||||
status: 'active',
|
status: 'active',
|
||||||
arbiter_verdict: null,
|
arbiter_verdict: null,
|
||||||
arbiter_requested_at: null,
|
arbiter_requested_at: null,
|
||||||
|
completion_reason: null,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
});
|
});
|
||||||
@@ -249,6 +251,7 @@ describe('paired workspace manager', () => {
|
|||||||
status: 'active',
|
status: 'active',
|
||||||
arbiter_verdict: null,
|
arbiter_verdict: null,
|
||||||
arbiter_requested_at: null,
|
arbiter_requested_at: null,
|
||||||
|
completion_reason: null,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
});
|
});
|
||||||
@@ -326,6 +329,7 @@ describe('paired workspace manager', () => {
|
|||||||
status: 'active',
|
status: 'active',
|
||||||
arbiter_verdict: null,
|
arbiter_verdict: null,
|
||||||
arbiter_requested_at: null,
|
arbiter_requested_at: null,
|
||||||
|
completion_reason: null,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
});
|
});
|
||||||
@@ -469,6 +473,7 @@ describe('paired workspace manager', () => {
|
|||||||
status: 'active',
|
status: 'active',
|
||||||
arbiter_verdict: null,
|
arbiter_verdict: null,
|
||||||
arbiter_requested_at: null,
|
arbiter_requested_at: null,
|
||||||
|
completion_reason: null,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
});
|
});
|
||||||
@@ -533,6 +538,7 @@ describe('paired workspace manager', () => {
|
|||||||
status: 'review_ready',
|
status: 'review_ready',
|
||||||
arbiter_verdict: null,
|
arbiter_verdict: null,
|
||||||
arbiter_requested_at: null,
|
arbiter_requested_at: null,
|
||||||
|
completion_reason: null,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: '2026-03-28T00:01:00.000Z',
|
updated_at: '2026-03-28T00:01:00.000Z',
|
||||||
});
|
});
|
||||||
@@ -601,6 +607,7 @@ describe('paired workspace manager', () => {
|
|||||||
status: 'active',
|
status: 'active',
|
||||||
arbiter_verdict: null,
|
arbiter_verdict: null,
|
||||||
arbiter_requested_at: null,
|
arbiter_requested_at: null,
|
||||||
|
completion_reason: null,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export interface PairedTask {
|
|||||||
status: PairedTaskStatus;
|
status: PairedTaskStatus;
|
||||||
arbiter_verdict: string | null;
|
arbiter_verdict: string | null;
|
||||||
arbiter_requested_at: string | null;
|
arbiter_requested_at: string | null;
|
||||||
|
completion_reason: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user