fix(paired): stop silent halt on reviewer PROCEED + reviewer-unavailable

Two causes of the paired room "keeps stopping" symptom:
- Reviewer approvals worded as "PROCEED" were parsed as 'continue'
  (a change request), causing an owner TASK_DONE <-> reviewer PROCEED
  ping-pong until the deadlock cap. parseReviewerVerdict() now treats a
  leading PROCEED as approval so the turn finalizes after one round.
- When the Codex reviewer was unavailable, the owner's answer was held
  for review and the user saw nothing. Now the held owner answer is
  emitted with a "review skipped" notice on reviewer_codex_unavailable.

Verified: tsc --noEmit clean; 27 related vitest tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-06-22 19:30:22 +09:00
parent 5d60df8122
commit 4d3ab20378
7 changed files with 256 additions and 5 deletions

View File

@@ -104,6 +104,34 @@ describe('paired execution routing loop guards', () => {
);
});
it('treats a reviewer PROCEED as approval and routes to owner finalize', () => {
// Regression: Codex reviewers approve with an arbiter-style "PROCEED" line.
// It used to parse as 'continue' (a change request), so the reviewer's
// approval bounced the task back to active and owner↔reviewer ping-ponged
// (owner TASK_DONE ↔ reviewer PROCEED) until the deadlock cap. A PROCEED
// approval must move the task to merge_ready so the owner can finalize.
vi.mocked(db.getPairedTaskById).mockReturnValue(
buildPairedTask({
status: 'in_review',
source_ref: 'approved-ref',
}),
);
completePairedExecutionContext({
taskId: 'task-1',
role: 'reviewer',
status: 'succeeded',
summary: 'PROCEED\n위치와 첨부는 맞습니다.',
});
expect(db.updatePairedTask).toHaveBeenCalledWith(
'task-1',
expect.objectContaining({
status: 'merge_ready',
}),
);
});
it('clears stale owner loop state when reviewer approval is recovered from a failed run', () => {
vi.mocked(db.getPairedTaskById).mockReturnValue(
buildPairedTask({