diff --git a/apps/api/src/continuation-prompts/continuation-prompts.module.ts b/apps/api/src/continuation-prompts/continuation-prompts.module.ts new file mode 100644 index 0000000..fad32d6 --- /dev/null +++ b/apps/api/src/continuation-prompts/continuation-prompts.module.ts @@ -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 {} diff --git a/apps/api/src/continuation-prompts/continuation-prompts.service.spec.ts b/apps/api/src/continuation-prompts/continuation-prompts.service.spec.ts new file mode 100644 index 0000000..339dbde --- /dev/null +++ b/apps/api/src/continuation-prompts/continuation-prompts.service.spec.ts @@ -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"); + }); + }); +}); diff --git a/apps/api/src/continuation-prompts/continuation-prompts.service.ts b/apps/api/src/continuation-prompts/continuation-prompts.service.ts new file mode 100644 index 0000000..bfcdbe4 --- /dev/null +++ b/apps/api/src/continuation-prompts/continuation-prompts.service.ts @@ -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"; + } +} diff --git a/apps/api/src/continuation-prompts/index.ts b/apps/api/src/continuation-prompts/index.ts new file mode 100644 index 0000000..0327f29 --- /dev/null +++ b/apps/api/src/continuation-prompts/index.ts @@ -0,0 +1,3 @@ +export * from "./continuation-prompts.module"; +export * from "./continuation-prompts.service"; +export * from "./interfaces"; diff --git a/apps/api/src/continuation-prompts/interfaces/continuation-prompt.interface.ts b/apps/api/src/continuation-prompts/interfaces/continuation-prompt.interface.ts new file mode 100644 index 0000000..6e79b76 --- /dev/null +++ b/apps/api/src/continuation-prompts/interfaces/continuation-prompt.interface.ts @@ -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"; +} diff --git a/apps/api/src/continuation-prompts/interfaces/index.ts b/apps/api/src/continuation-prompts/interfaces/index.ts new file mode 100644 index 0000000..df040b4 --- /dev/null +++ b/apps/api/src/continuation-prompts/interfaces/index.ts @@ -0,0 +1 @@ +export * from "./continuation-prompt.interface"; diff --git a/apps/api/src/continuation-prompts/templates/base.template.ts b/apps/api/src/continuation-prompts/templates/base.template.ts new file mode 100644 index 0000000..40b08ab --- /dev/null +++ b/apps/api/src/continuation-prompts/templates/base.template.ts @@ -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}} +`; diff --git a/apps/api/src/continuation-prompts/templates/build-error.template.ts b/apps/api/src/continuation-prompts/templates/build-error.template.ts new file mode 100644 index 0000000..a1e347b --- /dev/null +++ b/apps/api/src/continuation-prompts/templates/build-error.template.ts @@ -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 +`; diff --git a/apps/api/src/continuation-prompts/templates/coverage.template.ts b/apps/api/src/continuation-prompts/templates/coverage.template.ts new file mode 100644 index 0000000..d277e32 --- /dev/null +++ b/apps/api/src/continuation-prompts/templates/coverage.template.ts @@ -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 +`; diff --git a/apps/api/src/continuation-prompts/templates/incomplete-work.template.ts b/apps/api/src/continuation-prompts/templates/incomplete-work.template.ts new file mode 100644 index 0000000..a4b62e5 --- /dev/null +++ b/apps/api/src/continuation-prompts/templates/incomplete-work.template.ts @@ -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 +`; diff --git a/apps/api/src/continuation-prompts/templates/index.ts b/apps/api/src/continuation-prompts/templates/index.ts new file mode 100644 index 0000000..4c9da9c --- /dev/null +++ b/apps/api/src/continuation-prompts/templates/index.ts @@ -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"; diff --git a/apps/api/src/continuation-prompts/templates/test-failure.template.ts b/apps/api/src/continuation-prompts/templates/test-failure.template.ts new file mode 100644 index 0000000..8b87028 --- /dev/null +++ b/apps/api/src/continuation-prompts/templates/test-failure.template.ts @@ -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 +`;