feat(#172): Implement Herald status updates
Implements status broadcasting via bridge module to chat channels. The Herald service subscribes to job events and broadcasts status updates to Discord threads using PDA-friendly language. Features: - Herald module with HeraldService for status broadcasting - Subscribe to job lifecycle, step lifecycle, and gate events - Format messages with PDA-friendly language (no "FAILED", "URGENT", etc.) - Visual indicators for quick scanning (🟢, 🔵, ✅, ⚠️, ⏸️) - Channel selection logic via workspace settings - Route to Discord threads based on job metadata - Comprehensive unit tests (14 tests passing, 85%+ coverage) Message format examples: - Job created: 🟢 Job created for #42 - Job started: 🔵 Job started for #42 - Job completed: ✅ Job completed for #42 (120s) - Job failed: ⚠️ Job encountered an issue for #42 - Gate passed: ✅ Gate passed: build - Gate failed: ⚠️ Gate needs attention: test Quality gates: ✅ typecheck, lint, test, build PR comment support deferred - requires GitHub/Gitea API client implementation. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -145,4 +145,87 @@ export class JobStepsService {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a step - simplified API without jobId
|
||||
*/
|
||||
async start(id: string): Promise<Awaited<ReturnType<typeof this.prisma.jobStep.update>>> {
|
||||
const step = await this.prisma.jobStep.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!step) {
|
||||
throw new NotFoundException(`JobStep with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return this.startStep(id, step.jobId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete a step - simplified API without jobId
|
||||
*/
|
||||
async complete(
|
||||
id: string,
|
||||
data?: { output?: string; tokensInput?: number; tokensOutput?: number }
|
||||
): Promise<Awaited<ReturnType<typeof this.prisma.jobStep.update>>> {
|
||||
const step = await this.prisma.jobStep.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!step) {
|
||||
throw new NotFoundException(`JobStep with ID ${id} not found`);
|
||||
}
|
||||
|
||||
const existingStep = await this.findOne(id, step.jobId);
|
||||
const completedAt = new Date();
|
||||
const durationMs = existingStep.startedAt
|
||||
? completedAt.getTime() - existingStep.startedAt.getTime()
|
||||
: null;
|
||||
|
||||
const updateData: Prisma.JobStepUpdateInput = {
|
||||
status: JobStepStatus.COMPLETED,
|
||||
completedAt,
|
||||
durationMs,
|
||||
};
|
||||
|
||||
if (data?.output !== undefined) {
|
||||
updateData.output = data.output;
|
||||
}
|
||||
if (data?.tokensInput !== undefined) {
|
||||
updateData.tokensInput = data.tokensInput;
|
||||
}
|
||||
if (data?.tokensOutput !== undefined) {
|
||||
updateData.tokensOutput = data.tokensOutput;
|
||||
}
|
||||
|
||||
return this.prisma.jobStep.update({
|
||||
where: { id, jobId: step.jobId },
|
||||
data: updateData,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fail a step - simplified API without jobId
|
||||
*/
|
||||
async fail(
|
||||
id: string,
|
||||
data?: { error?: string }
|
||||
): Promise<Awaited<ReturnType<typeof this.prisma.jobStep.update>>> {
|
||||
const step = await this.prisma.jobStep.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!step) {
|
||||
throw new NotFoundException(`JobStep with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return this.failStep(id, step.jobId, data?.error ?? "Step failed");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get steps by job - alias for findAllByJob
|
||||
*/
|
||||
async findByJob(jobId: string): Promise<Awaited<ReturnType<typeof this.findAllByJob>>> {
|
||||
return this.findAllByJob(jobId);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user