Release: CI/CD Pipeline & Architecture Updates #177

Merged
jason.woltje merged 173 commits from develop into main 2026-02-01 19:18:48 +00:00
12 changed files with 704 additions and 0 deletions
Showing only changes of commit 0387cce116 - Show all commits

View File

@@ -0,0 +1,12 @@
import { Module } from "@nestjs/common";
import { ContinuationPromptsService } from "./continuation-prompts.service";
/**
* Continuation Prompts Module
* Generates forced continuation prompts for incomplete AI agent work
*/
@Module({
providers: [ContinuationPromptsService],
exports: [ContinuationPromptsService],
})
export class ContinuationPromptsModule {}

View File

@@ -0,0 +1,387 @@
import { describe, it, expect, beforeEach } from "vitest";
import { ContinuationPromptsService } from "./continuation-prompts.service";
import { ContinuationPromptContext, FailureDetail, ContinuationPrompt } from "./interfaces";
describe("ContinuationPromptsService", () => {
let service: ContinuationPromptsService;
let baseContext: ContinuationPromptContext;
beforeEach(() => {
service = new ContinuationPromptsService();
baseContext = {
taskId: "task-1",
originalTask: "Implement user authentication",
attemptNumber: 1,
maxAttempts: 3,
failures: [],
filesChanged: ["src/auth/auth.service.ts"],
};
});
describe("generatePrompt", () => {
it("should generate a prompt with system and user sections", () => {
const context: ContinuationPromptContext = {
...baseContext,
failures: [
{
type: "test-failure",
message: "Test failed: should authenticate user",
details: "Expected 200, got 401",
},
],
};
const prompt = service.generatePrompt(context);
expect(prompt).toBeDefined();
expect(prompt.systemPrompt).toContain("CRITICAL RULES");
expect(prompt.userPrompt).toContain("Implement user authentication");
expect(prompt.userPrompt).toContain("Test failed");
expect(prompt.constraints).toBeInstanceOf(Array);
expect(prompt.priority).toBe("high");
});
it("should include attempt number in prompt", () => {
const context: ContinuationPromptContext = {
...baseContext,
attemptNumber: 2,
failures: [
{
type: "build-error",
message: "Type error in auth.service.ts",
},
],
};
const prompt = service.generatePrompt(context);
expect(prompt.userPrompt).toContain("attempt 2 of 3");
});
it("should escalate priority on final attempt", () => {
const context: ContinuationPromptContext = {
...baseContext,
attemptNumber: 3,
maxAttempts: 3,
failures: [
{
type: "test-failure",
message: "Tests still failing",
},
],
};
const prompt = service.generatePrompt(context);
expect(prompt.priority).toBe("critical");
expect(prompt.constraints).toContain(
"This is your LAST attempt. Failure means manual intervention required."
);
});
it("should handle multiple failure types", () => {
const context: ContinuationPromptContext = {
...baseContext,
failures: [
{
type: "test-failure",
message: "Auth test failed",
},
{
type: "build-error",
message: "Type error",
},
{
type: "coverage",
message: "Coverage below 85%",
},
],
};
const prompt = service.generatePrompt(context);
expect(prompt.userPrompt).toContain("Auth test failed");
expect(prompt.userPrompt).toContain("Type error");
expect(prompt.userPrompt).toContain("Coverage below 85%");
});
});
describe("generateTestFailurePrompt", () => {
it("should format test failures with details", () => {
const failures: FailureDetail[] = [
{
type: "test-failure",
message: "should authenticate user",
details: "Expected 200, got 401",
location: "auth.service.spec.ts:42",
},
{
type: "test-failure",
message: "should reject invalid credentials",
details: "AssertionError: expected false to be true",
location: "auth.service.spec.ts:58",
},
];
const prompt = service.generateTestFailurePrompt(failures);
expect(prompt).toContain("should authenticate user");
expect(prompt).toContain("Expected 200, got 401");
expect(prompt).toContain("auth.service.spec.ts:42");
expect(prompt).toContain("should reject invalid credentials");
expect(prompt).toContain("Fix the implementation");
});
it("should include guidance for fixing tests", () => {
const failures: FailureDetail[] = [
{
type: "test-failure",
message: "Test failed",
},
];
const prompt = service.generateTestFailurePrompt(failures);
expect(prompt).toContain("Read the test");
expect(prompt).toContain("Fix the implementation");
expect(prompt).toContain("Run the test");
});
});
describe("generateBuildErrorPrompt", () => {
it("should format build errors with location", () => {
const failures: FailureDetail[] = [
{
type: "build-error",
message: "Type 'string' is not assignable to type 'number'",
location: "auth.service.ts:25",
},
{
type: "build-error",
message: "Cannot find name 'User'",
location: "auth.service.ts:42",
suggestion: "Import User from '@/entities'",
},
];
const prompt = service.generateBuildErrorPrompt(failures);
expect(prompt).toContain("Type 'string' is not assignable");
expect(prompt).toContain("auth.service.ts:25");
expect(prompt).toContain("Cannot find name 'User'");
expect(prompt).toContain("Import User from");
});
it("should include build-specific guidance", () => {
const failures: FailureDetail[] = [
{
type: "build-error",
message: "Syntax error",
},
];
const prompt = service.generateBuildErrorPrompt(failures);
expect(prompt).toContain("TypeScript");
expect(prompt).toContain("Do not proceed until build passes");
});
});
describe("generateCoveragePrompt", () => {
it("should show coverage gap", () => {
const prompt = service.generateCoveragePrompt(72, 85);
expect(prompt).toContain("72%");
expect(prompt).toContain("85%");
expect(prompt).toContain("13%"); // gap
});
it("should provide guidance for improving coverage", () => {
const prompt = service.generateCoveragePrompt(80, 85);
expect(prompt).toContain("uncovered code paths");
expect(prompt).toContain("edge cases");
expect(prompt).toContain("error handling");
});
});
describe("generateIncompleteWorkPrompt", () => {
it("should list incomplete work items", () => {
const issues = [
"TODO: Implement password hashing",
"FIXME: Add error handling",
"Missing validation for email format",
];
const prompt = service.generateIncompleteWorkPrompt(issues);
expect(prompt).toContain("TODO: Implement password hashing");
expect(prompt).toContain("FIXME: Add error handling");
expect(prompt).toContain("Missing validation");
});
it("should emphasize completion requirement", () => {
const issues = ["Missing feature X"];
const prompt = service.generateIncompleteWorkPrompt(issues);
expect(prompt).toContain("MUST complete ALL aspects");
expect(prompt).toContain("Do not leave TODO");
});
});
describe("getConstraints", () => {
it("should return basic constraints for first attempt", () => {
const constraints = service.getConstraints(1, 3);
expect(constraints).toBeInstanceOf(Array);
expect(constraints.length).toBeGreaterThan(0);
});
it("should escalate constraints on second attempt", () => {
const constraints = service.getConstraints(2, 3);
expect(constraints).toContain("Focus only on failures, no new features");
});
it("should add strict constraints on third attempt", () => {
const constraints = service.getConstraints(3, 3);
expect(constraints).toContain("Minimal changes only, fix exact issues");
});
it("should add final warning on last attempt", () => {
const constraints = service.getConstraints(3, 3);
expect(constraints).toContain(
"This is your LAST attempt. Failure means manual intervention required."
);
});
it("should handle different max attempts", () => {
const constraints = service.getConstraints(5, 5);
expect(constraints).toContain(
"This is your LAST attempt. Failure means manual intervention required."
);
});
});
describe("formatFailuresForPrompt", () => {
it("should format failures with all details", () => {
const failures: FailureDetail[] = [
{
type: "test-failure",
message: "Test failed",
details: "Expected true, got false",
location: "file.spec.ts:10",
suggestion: "Check the implementation",
},
];
const formatted = service.formatFailuresForPrompt(failures);
expect(formatted).toContain("test-failure");
expect(formatted).toContain("Test failed");
expect(formatted).toContain("Expected true, got false");
expect(formatted).toContain("file.spec.ts:10");
expect(formatted).toContain("Check the implementation");
});
it("should handle failures without optional fields", () => {
const failures: FailureDetail[] = [
{
type: "lint-error",
message: "Unused variable",
},
];
const formatted = service.formatFailuresForPrompt(failures);
expect(formatted).toContain("lint-error");
expect(formatted).toContain("Unused variable");
});
it("should format multiple failures", () => {
const failures: FailureDetail[] = [
{
type: "test-failure",
message: "Test 1 failed",
},
{
type: "build-error",
message: "Build error",
},
{
type: "coverage",
message: "Low coverage",
},
];
const formatted = service.formatFailuresForPrompt(failures);
expect(formatted).toContain("Test 1 failed");
expect(formatted).toContain("Build error");
expect(formatted).toContain("Low coverage");
});
it("should handle empty failures array", () => {
const failures: FailureDetail[] = [];
const formatted = service.formatFailuresForPrompt(failures);
expect(formatted).toBe("");
});
});
describe("priority assignment", () => {
it("should set normal priority for first attempt with minor issues", () => {
const context: ContinuationPromptContext = {
...baseContext,
attemptNumber: 1,
failures: [
{
type: "lint-error",
message: "Minor lint issue",
},
],
};
const prompt = service.generatePrompt(context);
expect(prompt.priority).toBe("normal");
});
it("should set high priority for build errors", () => {
const context: ContinuationPromptContext = {
...baseContext,
failures: [
{
type: "build-error",
message: "Build failed",
},
],
};
const prompt = service.generatePrompt(context);
expect(prompt.priority).toBe("high");
});
it("should set high priority for test failures", () => {
const context: ContinuationPromptContext = {
...baseContext,
failures: [
{
type: "test-failure",
message: "Test failed",
},
],
};
const prompt = service.generatePrompt(context);
expect(prompt.priority).toBe("high");
});
});
});

View File

@@ -0,0 +1,207 @@
import { Injectable } from "@nestjs/common";
import { ContinuationPromptContext, FailureDetail, ContinuationPrompt } from "./interfaces";
import {
BASE_CONTINUATION_SYSTEM,
BASE_USER_PROMPT,
TEST_FAILURE_TEMPLATE,
BUILD_ERROR_TEMPLATE,
COVERAGE_TEMPLATE,
INCOMPLETE_WORK_TEMPLATE,
} from "./templates";
/**
* Service for generating continuation prompts when AI agent work is incomplete
*/
@Injectable()
export class ContinuationPromptsService {
/**
* Generate a complete continuation prompt from context
*/
generatePrompt(context: ContinuationPromptContext): ContinuationPrompt {
const systemPrompt = BASE_CONTINUATION_SYSTEM;
const constraints = this.getConstraints(context.attemptNumber, context.maxAttempts);
// Format failures based on their types
const formattedFailures = this.formatFailuresByType(context.failures);
// Build user prompt
const userPrompt = BASE_USER_PROMPT.replace("{{taskDescription}}", context.originalTask)
.replace("{{attemptNumber}}", String(context.attemptNumber))
.replace("{{maxAttempts}}", String(context.maxAttempts))
.replace("{{failures}}", formattedFailures)
.replace("{{constraints}}", this.formatConstraints(constraints));
// Determine priority
const priority = this.determinePriority(context);
return {
systemPrompt,
userPrompt,
constraints,
priority,
};
}
/**
* Generate test failure specific prompt
*/
generateTestFailurePrompt(failures: FailureDetail[]): string {
const formattedFailures = this.formatFailuresForPrompt(failures);
return TEST_FAILURE_TEMPLATE.replace("{{failures}}", formattedFailures);
}
/**
* Generate build error specific prompt
*/
generateBuildErrorPrompt(failures: FailureDetail[]): string {
const formattedErrors = this.formatFailuresForPrompt(failures);
return BUILD_ERROR_TEMPLATE.replace("{{errors}}", formattedErrors);
}
/**
* Generate coverage improvement prompt
*/
generateCoveragePrompt(current: number, required: number): string {
const gap = required - current;
return COVERAGE_TEMPLATE.replace("{{currentCoverage}}", String(current))
.replace("{{requiredCoverage}}", String(required))
.replace("{{gap}}", String(gap))
.replace("{{uncoveredFiles}}", "(See coverage report for details)");
}
/**
* Generate incomplete work prompt
*/
generateIncompleteWorkPrompt(issues: string[]): string {
const formattedIssues = issues.map((issue) => `- ${issue}`).join("\n");
return INCOMPLETE_WORK_TEMPLATE.replace("{{issues}}", formattedIssues);
}
/**
* Get constraints based on attempt number
*/
getConstraints(attemptNumber: number, maxAttempts: number): string[] {
const constraints: string[] = [
"Address ALL failures listed above",
"Run all quality checks before claiming completion",
];
if (attemptNumber >= 2) {
constraints.push("Focus only on failures, no new features");
}
if (attemptNumber >= 3) {
constraints.push("Minimal changes only, fix exact issues");
}
if (attemptNumber >= maxAttempts) {
constraints.push("This is your LAST attempt. Failure means manual intervention required.");
}
return constraints;
}
/**
* Format failures for inclusion in prompt
*/
formatFailuresForPrompt(failures: FailureDetail[]): string {
if (failures.length === 0) {
return "";
}
return failures
.map((failure, index) => {
const parts: string[] = [`${String(index + 1)}. [${failure.type}] ${failure.message}`];
if (failure.location) {
parts.push(` Location: ${failure.location}`);
}
if (failure.details) {
parts.push(` Details: ${failure.details}`);
}
if (failure.suggestion) {
parts.push(` Suggestion: ${failure.suggestion}`);
}
return parts.join("\n");
})
.join("\n\n");
}
/**
* Format failures by type using appropriate templates
*/
private formatFailuresByType(failures: FailureDetail[]): string {
const sections: string[] = [];
// Group failures by type
const testFailures = failures.filter((f) => f.type === "test-failure");
const buildErrors = failures.filter((f) => f.type === "build-error");
const coverageIssues = failures.filter((f) => f.type === "coverage");
const incompleteWork = failures.filter((f) => f.type === "incomplete-work");
const lintErrors = failures.filter((f) => f.type === "lint-error");
if (testFailures.length > 0) {
sections.push(this.generateTestFailurePrompt(testFailures));
}
if (buildErrors.length > 0) {
sections.push(this.generateBuildErrorPrompt(buildErrors));
}
if (coverageIssues.length > 0) {
// Extract coverage numbers from message if available
const coverageFailure = coverageIssues[0];
if (coverageFailure) {
const match = /(\d+)%.*?(\d+)%/.exec(coverageFailure.message);
if (match?.[1] && match[2]) {
sections.push(this.generateCoveragePrompt(parseInt(match[1]), parseInt(match[2])));
} else {
sections.push(this.formatFailuresForPrompt(coverageIssues));
}
}
}
if (incompleteWork.length > 0) {
const issues = incompleteWork.map((f) => f.message);
sections.push(this.generateIncompleteWorkPrompt(issues));
}
if (lintErrors.length > 0) {
sections.push("Lint Errors:\n" + this.formatFailuresForPrompt(lintErrors));
}
return sections.join("\n\n---\n\n");
}
/**
* Format constraints as a bulleted list
*/
private formatConstraints(constraints: string[]): string {
return "CONSTRAINTS:\n" + constraints.map((c) => `- ${c}`).join("\n");
}
/**
* Determine priority based on context
*/
private determinePriority(context: ContinuationPromptContext): "critical" | "high" | "normal" {
// Final attempt is always critical
if (context.attemptNumber >= context.maxAttempts) {
return "critical";
}
// Build errors and test failures are high priority
const hasCriticalFailures = context.failures.some(
(f) => f.type === "build-error" || f.type === "test-failure"
);
if (hasCriticalFailures) {
return "high";
}
// Everything else is normal
return "normal";
}
}

View File

@@ -0,0 +1,3 @@
export * from "./continuation-prompts.module";
export * from "./continuation-prompts.service";
export * from "./interfaces";

View File

@@ -0,0 +1,24 @@
export interface ContinuationPromptContext {
taskId: string;
originalTask: string;
attemptNumber: number;
maxAttempts: number;
failures: FailureDetail[];
previousOutput?: string;
filesChanged: string[];
}
export interface FailureDetail {
type: "test-failure" | "build-error" | "lint-error" | "coverage" | "incomplete-work";
message: string;
details?: string;
location?: string; // file:line
suggestion?: string;
}
export interface ContinuationPrompt {
systemPrompt: string;
userPrompt: string;
constraints: string[];
priority: "critical" | "high" | "normal";
}

View File

@@ -0,0 +1 @@
export * from "./continuation-prompt.interface";

View File

@@ -0,0 +1,18 @@
export const BASE_CONTINUATION_SYSTEM = `You are continuing work on a task that was not completed successfully.
Your previous attempt did not pass quality gates. You MUST fix the issues below.
CRITICAL RULES:
1. You MUST address EVERY failure listed
2. Do NOT defer work to future tasks
3. Do NOT claim done until all gates pass
4. Run tests before claiming completion
`;
export const BASE_USER_PROMPT = `Task: {{taskDescription}}
Previous attempt {{attemptNumber}} of {{maxAttempts}} did not pass quality gates.
{{failures}}
{{constraints}}
`;

View File

@@ -0,0 +1,10 @@
export const BUILD_ERROR_TEMPLATE = `Build errors detected:
{{errors}}
Fix these TypeScript/compilation errors. Do not proceed until build passes.
Steps:
1. Read the error messages carefully
2. Fix type mismatches, missing imports, or syntax errors
3. Run build to verify it passes
`;

View File

@@ -0,0 +1,15 @@
export const COVERAGE_TEMPLATE = `Test coverage is below required threshold.
Current coverage: {{currentCoverage}}%
Required coverage: {{requiredCoverage}}%
Gap: {{gap}}%
Files with insufficient coverage:
{{uncoveredFiles}}
Steps to improve coverage:
1. Identify uncovered code paths
2. Write tests for uncovered scenarios
3. Focus on edge cases and error handling
4. Run coverage report to verify improvement
`;

View File

@@ -0,0 +1,13 @@
export const INCOMPLETE_WORK_TEMPLATE = `The task implementation is incomplete.
Issues detected:
{{issues}}
You MUST complete ALL aspects of the task. Do not leave TODO comments or deferred work.
Steps:
1. Review each incomplete item
2. Implement the missing functionality
3. Write tests for the new code
4. Verify all requirements are met
`;

View File

@@ -0,0 +1,5 @@
export * from "./base.template";
export * from "./test-failure.template";
export * from "./build-error.template";
export * from "./coverage.template";
export * from "./incomplete-work.template";

View File

@@ -0,0 +1,9 @@
export const TEST_FAILURE_TEMPLATE = `The following tests are failing:
{{failures}}
For each failing test:
1. Read the test to understand what is expected
2. Fix the implementation to pass the test
3. Run the test to verify it passes
4. Do NOT skip or modify tests - fix the implementation
`;