task-validation.ts
1 import type { AppSettings, BoardTask } from '@/types' 2 import type { TaskReportArtifact } from '@/lib/server/tasks/task-reports' 3 import { normalizeTaskQualityGate } from '@/lib/server/tasks/task-quality-gate' 4 5 export interface TaskCompletionValidation { 6 ok: boolean 7 reasons: string[] 8 checkedAt: number 9 } 10 11 interface TaskCompletionValidationOptions { 12 report?: TaskReportArtifact | null 13 settings?: AppSettings | Record<string, unknown> | null 14 } 15 16 const MIN_RESULT_CHARS_IMPLEMENTATION = 40 17 const MIN_RESULT_CHARS_GENERIC = 20 18 19 const SHORT_ANSWER_REQUEST_PATTERNS: RegExp[] = [ 20 /\bone[\s-]word\b/i, 21 /\bsingle[\s-]word\b/i, 22 /\byes\s+or\s+no\b/i, 23 /\bin\s+(?:a\s+)?single\s+(?:word|line|sentence)\b/i, 24 /\bin\s+\d+\s+(?:word|words|chars?|characters?)\b/i, 25 /\breply\s+with\s+(?:the\s+(?:word|number|letter|character|string)\s+)?["'`]?[\w-]{1,40}["'`]?\s+only\b/i, 26 /\brespond\s+with\s+(?:the\s+(?:word|number|letter|character|string)\s+)?["'`]?[\w-]{1,40}["'`]?\s+only\b/i, 27 /\b(?:answer|reply|respond)\s+(?:with\s+)?["'`][^"'`]{1,40}["'`]\s+only\b/i, 28 /\bjust\s+(?:say|reply|return|output|answer)\b/i, 29 ] 30 31 function promptRequestsShortAnswer(text: string): boolean { 32 if (!text) return false 33 return SHORT_ANSWER_REQUEST_PATTERNS.some((rx) => rx.test(text)) 34 } 35 36 const WEAK_RESULT_PATTERNS: RegExp[] = [ 37 /what can i help you with/i, 38 /waiting for approval/i, 39 /now let me write/i, 40 /what'?s the play/i, 41 /\bthe plan covers\b/i, 42 /now update the agent/i, 43 /\bzero typescript errors\b/i, 44 ] 45 46 const INCOMPLETE_RESULT_PATTERNS: RegExp[] = [ 47 /\b(?:next|then)\s*,?\s*i\s+(?:will|can|am going to)\b/i, 48 /\b(?:i(?:'| a)?ll|let me)\s+(?:start|begin|proceed|continue)\b/i, 49 /\b(?:once|when|after)\s+(?:the\s+)?(?:access|approval|permission)\s+(?:is|has been)\s+granted\b/i, 50 /\bneed (?:more )?(?:details|information|context)\b/i, 51 /\b(?:i|we)\s+(?:need|require)\s+(?:access|approval|permission)\b/i, 52 /\brequested\s+(?:access|approval|permission)\b/i, 53 /\bneed access to (?:the )?(?:shell|terminal|command line)\b/i, 54 /\battempted to\b[^.]{0,120}\b(?:but|however)\b/i, 55 /\bcould you provide\b/i, 56 /\blet me know once\b/i, 57 /\bthere (?:aren't|are not) any specific details\b/i, 58 ] 59 60 const IMPLEMENTATION_HINT = /\b(add|build|create|fix|implement|integrat|refactor|update|write)\b/i 61 const EXECUTION_ACTION_HINT = /\b(changed|updated|added|modified|implemented|refactored|fixed|ran|executed|verified)\b/i 62 const COMMAND_EVIDENCE_HINT = /\b(npm|pnpm|yarn|bun|node|npx|pytest|vitest|jest|playwright|go test|cargo test|deno test|python|pip|uv|docker|git)\b/i 63 const FILE_PATH_EVIDENCE_HINT = /\b[\w./-]+\.(ts|tsx|js|jsx|mjs|cjs|json|md|css|scss|html|yml|yaml|sh|py|go|rs|java|kt|swift|rb|php|sql|txt)\b/i 64 const ARTIFACT_EVIDENCE_HINT = /(?:sandbox:)?\/api\/uploads\/[^\s)\]]+|https?:\/\/[^\s)\]]+\.(?:png|jpe?g|webp|gif|pdf|zip)\b/i 65 const VERIFICATION_EVIDENCE_HINT = /\b(test|tests|lint|typecheck|build)\b[^.]{0,40}\b(pass(?:ed)?|fail(?:ed)?|ok|success)\b/i 66 const SCREENSHOT_HINT = /\b(screenshot|screen shot|snapshot|capture)\b/i 67 const DELIVERY_HINT = /\b(send|deliver|return|share|upload|post|message)\b/i 68 const SCREENSHOT_ARTIFACT_HINT = /(?:sandbox:)?\/api\/uploads\/[^\s)\]]+|https?:\/\/[^\s)\]]+\.(?:png|jpe?g|webp|gif|pdf)\b/i 69 const SENT_SCREENSHOT_HINT = /\b(sent|shared|uploaded|returned)\b[^.]*\b(screenshot|snapshot|image)\b/i 70 71 function normalizeText(value: unknown): string { 72 if (typeof value !== 'string') return '' 73 return value.replace(/\s+/g, ' ').trim() 74 } 75 76 export function validateTaskCompletion( 77 task: Partial<BoardTask>, 78 options: TaskCompletionValidationOptions = {}, 79 ): TaskCompletionValidation { 80 const reasons: string[] = [] 81 const title = normalizeText(task.title) 82 const description = normalizeText(task.description) 83 const result = normalizeText(task.result) 84 const error = normalizeText(task.error) 85 const report = options.report || null 86 const hasExplicitQualityGate = !!task.qualityGate && typeof task.qualityGate === 'object' 87 const qualityGate = normalizeTaskQualityGate(task.qualityGate || null, options.settings || null) 88 const implementationTask = IMPLEMENTATION_HINT.test(title) || IMPLEMENTATION_HINT.test(description) 89 90 if (error) reasons.push('Task has a non-empty error field.') 91 if (/^untitled task$/i.test(title) && !description) { 92 reasons.push('Task metadata is too vague (untitled title with empty description).') 93 } 94 95 const shortAnswerRequested = promptRequestsShortAnswer(`${title} ${description}`) 96 97 if (!result) reasons.push('Result summary is empty.') 98 else if (!shortAnswerRequested) { 99 const minChars = implementationTask ? MIN_RESULT_CHARS_IMPLEMENTATION : MIN_RESULT_CHARS_GENERIC 100 if (result.length < minChars) reasons.push(`Result summary is too short (${result.length} chars; min ${minChars}).`) 101 if (WEAK_RESULT_PATTERNS.some((rx) => rx.test(result))) { 102 reasons.push('Result contains placeholder/planning language instead of completion evidence.') 103 } 104 if (INCOMPLETE_RESULT_PATTERNS.some((rx) => rx.test(result))) { 105 reasons.push('Result indicates unfinished work or missing inputs instead of completed execution.') 106 } 107 } 108 109 // If task description/title suggests implementation work, require concrete evidence in 110 // the result summary OR task report. 111 const hasResultEvidence = ( 112 COMMAND_EVIDENCE_HINT.test(result) 113 || ARTIFACT_EVIDENCE_HINT.test(result) 114 || VERIFICATION_EVIDENCE_HINT.test(result) 115 || (EXECUTION_ACTION_HINT.test(result) 116 && (/\b(command|test|lint|typecheck|build|file|artifact)\b/i.test(result) || FILE_PATH_EVIDENCE_HINT.test(result))) 117 ) 118 const hasReportEvidence = report?.evidence.hasEvidence === true 119 if (implementationTask && !shortAnswerRequested && !hasResultEvidence && !hasReportEvidence) { 120 if (report?.relativePath) { 121 reasons.push(`Implementation task is missing concrete execution evidence in result or ${report.relativePath}.`) 122 } else { 123 reasons.push('Implementation task is missing concrete execution evidence in result.') 124 } 125 } 126 127 const screenshotTask = SCREENSHOT_HINT.test(title) || SCREENSHOT_HINT.test(description) 128 const screenshotDeliveryTask = screenshotTask && (DELIVERY_HINT.test(title) || DELIVERY_HINT.test(description)) 129 if (screenshotDeliveryTask) { 130 const hasScreenshotArtifact = SCREENSHOT_ARTIFACT_HINT.test(result) || SENT_SCREENSHOT_HINT.test(result) 131 if (!hasScreenshotArtifact) { 132 reasons.push('Screenshot delivery task is missing artifact evidence (upload link or explicit sent screenshot confirmation).') 133 } 134 } 135 136 if (qualityGate.enabled && (implementationTask || hasExplicitQualityGate)) { 137 if (result && result.length < qualityGate.minResultChars) { 138 reasons.push(`Quality gate: result summary is shorter than required minimum (${result.length} chars; min ${qualityGate.minResultChars}).`) 139 } 140 141 const hasCommandEvidence = COMMAND_EVIDENCE_HINT.test(result) || (report?.evidence.commandsRun.length || 0) > 0 142 const hasFileEvidence = FILE_PATH_EVIDENCE_HINT.test(result) || (report?.evidence.changedFiles.length || 0) > 0 143 const hasVerificationEvidence = VERIFICATION_EVIDENCE_HINT.test(result) || (report?.evidence.verification.length || 0) > 0 144 const hasArtifactEvidence = ARTIFACT_EVIDENCE_HINT.test(result) || ((task.artifacts?.length || 0) > 0) 145 146 const evidenceSignals = [ 147 hasCommandEvidence, 148 hasFileEvidence, 149 hasVerificationEvidence, 150 hasArtifactEvidence, 151 ].filter(Boolean).length 152 153 if (evidenceSignals < qualityGate.minEvidenceItems) { 154 reasons.push(`Quality gate: insufficient completion evidence (${evidenceSignals}/${qualityGate.minEvidenceItems} required evidence signals).`) 155 } 156 if (qualityGate.requireVerification && !hasVerificationEvidence) { 157 reasons.push('Quality gate: verification evidence is required (tests/lint/build/check output missing).') 158 } 159 if (qualityGate.requireArtifact && !hasArtifactEvidence) { 160 reasons.push('Quality gate: artifact evidence is required (artifact URL/upload or structured artifacts list missing).') 161 } 162 if (qualityGate.requireReport && !report?.relativePath) { 163 reasons.push('Quality gate: task completion report is required but missing.') 164 } 165 } 166 167 return { 168 ok: reasons.length === 0, 169 reasons, 170 checkedAt: Date.now(), 171 } 172 } 173 174 export function formatValidationFailure(reasons: string[]): string { 175 return `Completion validation failed: ${reasons.join(' ')}` 176 }