feat: add quality-review dispatch hook to intercept agent:stopped events
When an execute-mode agent stops with task_complete and the initiative has qualityReview=true, the orchestrator now spawns a fresh execute-mode agent to run /simplify on changed .ts/.tsx/.js files before marking the task completed. The task transitions through quality_review status as a recursion guard so the review agent's stop event is handled normally. - Add apps/server/execution/quality-review.ts with three exported functions: computeQualifyingFiles, shouldRunQualityReview, runQualityReview - Add apps/server/execution/quality-review.test.ts (28 tests) - Update ExecutionOrchestrator to accept agentManager, replace handleAgentStopped with quality-review-aware logic, add getRepoPathForTask - Update orchestrator.test.ts with 3 quality-review integration tests - Update container.ts to pass agentManager to ExecutionOrchestrator - Update docs/dispatch-events.md to reflect new agent:stopped behavior Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,12 +18,14 @@ import type { TaskRepository } from '../db/repositories/task-repository.js';
|
||||
import type { InitiativeRepository } from '../db/repositories/initiative-repository.js';
|
||||
import type { ProjectRepository } from '../db/repositories/project-repository.js';
|
||||
import type { AgentRepository } from '../db/repositories/agent-repository.js';
|
||||
import type { AgentManager } from '../agent/types.js';
|
||||
import type { DispatchManager, PhaseDispatchManager } from '../dispatch/types.js';
|
||||
import type { ConflictResolutionService } from '../coordination/conflict-resolution-service.js';
|
||||
import { phaseBranchName, taskBranchName } from '../git/branch-naming.js';
|
||||
import { ensureProjectClone } from '../git/project-clones.js';
|
||||
import { createModuleLogger } from '../logger/index.js';
|
||||
import { phaseMetaCache, fileDiffCache } from '../review/diff-cache.js';
|
||||
import { shouldRunQualityReview, runQualityReview } from './quality-review.js';
|
||||
|
||||
const log = createModuleLogger('execution-orchestrator');
|
||||
|
||||
@@ -49,6 +51,7 @@ export class ExecutionOrchestrator {
|
||||
private conflictResolutionService: ConflictResolutionService,
|
||||
private eventBus: EventBus,
|
||||
private workspaceRoot: string,
|
||||
private agentManager: AgentManager,
|
||||
private agentRepository?: AgentRepository,
|
||||
) {}
|
||||
|
||||
@@ -108,15 +111,53 @@ export class ExecutionOrchestrator {
|
||||
private async handleAgentStopped(event: AgentStoppedEvent): Promise<void> {
|
||||
const { taskId, reason, agentId } = event.payload;
|
||||
|
||||
// Auto-complete task for successful agent completions, not manual stops
|
||||
if (taskId && reason !== 'user_requested') {
|
||||
try {
|
||||
await this.dispatchManager.completeTask(taskId, agentId);
|
||||
log.info({ taskId, agentId, reason }, 'task auto-completed on agent stop');
|
||||
if (!this.agentRepository) {
|
||||
// No agent repository — skip quality review, complete task directly
|
||||
log.warn({ taskId, agentId }, 'agentRepository not available; skipping quality review');
|
||||
await this.dispatchManager.completeTask(taskId, agentId);
|
||||
log.info({ taskId, agentId, reason }, 'task auto-completed on agent stop');
|
||||
} else {
|
||||
// Get repoPath from first project in initiative (for branch diffing)
|
||||
const repoPath = await this.getRepoPathForTask(taskId);
|
||||
|
||||
const result = await shouldRunQualityReview({
|
||||
agentId,
|
||||
taskId,
|
||||
stopReason: reason,
|
||||
agentRepository: this.agentRepository,
|
||||
taskRepository: this.taskRepository,
|
||||
initiativeRepository: this.initiativeRepository,
|
||||
phaseRepository: this.phaseRepository,
|
||||
branchManager: this.branchManager,
|
||||
repoPath,
|
||||
});
|
||||
|
||||
if (result.run) {
|
||||
const task = await this.taskRepository.findById(taskId);
|
||||
const initiative = await this.initiativeRepository.findById(task!.initiativeId!);
|
||||
const phase = await this.phaseRepository.findById(task!.phaseId!);
|
||||
const initBranch = initiative!.branch!;
|
||||
await runQualityReview({
|
||||
taskId,
|
||||
taskBranch: taskBranchName(initBranch, taskId),
|
||||
baseBranch: phaseBranchName(initBranch, phase!.name),
|
||||
initiativeId: task!.initiativeId!,
|
||||
qualifyingFiles: result.qualifyingFiles,
|
||||
taskRepository: this.taskRepository,
|
||||
agentManager: this.agentManager,
|
||||
log,
|
||||
});
|
||||
} else {
|
||||
await this.dispatchManager.completeTask(taskId, agentId);
|
||||
log.info({ taskId, agentId, reason }, 'task auto-completed on agent stop');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
log.warn(
|
||||
{ taskId, agentId, reason, err: err instanceof Error ? err.message : String(err) },
|
||||
'failed to auto-complete task on agent stop',
|
||||
'failed to handle agent stop',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -124,6 +165,14 @@ export class ExecutionOrchestrator {
|
||||
this.scheduleDispatch();
|
||||
}
|
||||
|
||||
private async getRepoPathForTask(taskId: string): Promise<string> {
|
||||
const task = await this.taskRepository.findById(taskId);
|
||||
if (!task?.initiativeId) return this.workspaceRoot;
|
||||
const projects = await this.projectRepository.findProjectsByInitiativeId(task.initiativeId);
|
||||
if (!projects.length) return this.workspaceRoot;
|
||||
return ensureProjectClone(projects[0], this.workspaceRoot);
|
||||
}
|
||||
|
||||
private async handleAgentCrashed(event: AgentCrashedEvent): Promise<void> {
|
||||
const { taskId, agentId, error } = event.payload;
|
||||
if (!taskId) return;
|
||||
|
||||
Reference in New Issue
Block a user