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:
Eyejoker
2026-03-31 04:10:33 +09:00
parent b02c509eb8
commit e1b9501e87
9 changed files with 43 additions and 3 deletions

View File

@@ -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',
});

View File

@@ -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);

View File

@@ -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',
},

View File

@@ -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',
});

View File

@@ -846,6 +846,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
if (task) {
updatePairedTask(task.id, {
status: 'completed',
completion_reason: 'stopped',
updated_at: new Date().toISOString(),
});
}

View File

@@ -112,6 +112,7 @@ function buildPairedTask(overrides: Partial<PairedTask> = {}): 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',
});

View File

@@ -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;

View File

@@ -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,
});

View File

@@ -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;
}