feat: Wire full request-changes flow for phase review

- Add PhaseChangesRequestedEvent to event bus
- Add requestChangesOnPhase() to ExecutionOrchestrator: reads unresolved
  comments, creates revision task (category='review'), resets phase to
  in_progress, queues task for dispatch
- Expand merge-skip and branch routing to include 'review' category so
  revision tasks work directly on the phase branch
- Add requestPhaseChanges tRPC procedure (reads comments from DB)
- Wire frontend: mutation replaces stub handler, window.prompt for
  optional summary, loading state on button
This commit is contained in:
Lukas May
2026-03-05 11:35:34 +01:00
parent 8ae3916c90
commit 7e0749ef17
7 changed files with 153 additions and 11 deletions

View File

@@ -11,7 +11,7 @@
* - Review per-phase: pause after each phase for diff review
*/
import type { EventBus, TaskCompletedEvent, PhasePendingReviewEvent, PhaseMergedEvent, TaskMergedEvent, PhaseQueuedEvent, AgentStoppedEvent } from '../events/index.js';
import type { EventBus, TaskCompletedEvent, PhasePendingReviewEvent, PhaseChangesRequestedEvent, PhaseMergedEvent, TaskMergedEvent, PhaseQueuedEvent, AgentStoppedEvent } from '../events/index.js';
import type { BranchManager } from '../git/branch-manager.js';
import type { PhaseRepository } from '../db/repositories/phase-repository.js';
import type { TaskRepository } from '../db/repositories/task-repository.js';
@@ -145,8 +145,8 @@ export class ExecutionOrchestrator {
const phase = await this.phaseRepository.findById(task.phaseId);
if (!phase) return;
// Skip merge tasks — they already work on the phase branch directly
if (task.category === 'merge') return;
// Skip merge/review tasks — they already work on the phase branch directly
if (task.category === 'merge' || task.category === 'review') return;
const initBranch = initiative.branch;
const phBranch = phaseBranchName(initBranch, phase.name);
@@ -304,4 +304,84 @@ export class ExecutionOrchestrator {
log.info({ phaseId }, 'phase review approved and merged');
}
/**
* Request changes on a phase that's pending review.
* Creates a revision task, resets the phase to in_progress, and dispatches.
*/
async requestChangesOnPhase(
phaseId: string,
unresolvedComments: Array<{ filePath: string; lineNumber: number; body: string }>,
summary?: string,
): Promise<{ taskId: string }> {
const phase = await this.phaseRepository.findById(phaseId);
if (!phase) throw new Error(`Phase not found: ${phaseId}`);
if (phase.status !== 'pending_review') {
throw new Error(`Phase ${phaseId} is not pending review (status: ${phase.status})`);
}
const initiative = await this.initiativeRepository.findById(phase.initiativeId);
if (!initiative) throw new Error(`Initiative not found: ${phase.initiativeId}`);
// Build revision task description from comments + summary
const lines: string[] = [];
if (summary) {
lines.push(`## Summary\n\n${summary}\n`);
}
if (unresolvedComments.length > 0) {
lines.push('## Review Comments\n');
// Group comments by file
const byFile = new Map<string, typeof unresolvedComments>();
for (const c of unresolvedComments) {
const arr = byFile.get(c.filePath) ?? [];
arr.push(c);
byFile.set(c.filePath, arr);
}
for (const [filePath, fileComments] of byFile) {
lines.push(`### ${filePath}\n`);
for (const c of fileComments) {
lines.push(`- **Line ${c.lineNumber}**: ${c.body}`);
}
lines.push('');
}
}
const description = lines.join('\n') || 'Address review feedback.';
// Create revision task
const task = await this.taskRepository.create({
phaseId,
initiativeId: phase.initiativeId,
name: `Address review feedback: ${phase.name}`,
description,
category: 'review',
priority: 'high',
});
// Reset phase status back to in_progress
await this.phaseRepository.update(phaseId, { status: 'in_progress' as any });
// Queue task for dispatch
await this.dispatchManager.queue(task.id);
// Emit event
const event: PhaseChangesRequestedEvent = {
type: 'phase:changes_requested',
timestamp: new Date(),
payload: {
phaseId,
initiativeId: phase.initiativeId,
taskId: task.id,
commentCount: unresolvedComments.length,
},
};
this.eventBus.emit(event);
log.info({ phaseId, taskId: task.id, commentCount: unresolvedComments.length }, 'changes requested on phase');
// Kick off dispatch
this.scheduleDispatch();
return { taskId: task.id };
}
}