From 025920bc12580aa29bf888fbe6f1e194c67895b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 27 Mar 2026 00:27:06 -0400 Subject: [PATCH] feat(07-01): implement task quality gate validation module - validateTaskCreation: min 50 char description, heartbeat bypass, acceptance criteria check - checkAcceptanceCriteria: 12 keyword patterns for structured task definitions - checkCapabilityMatch: word-overlap matching with skill fallback and graceful degradation - All 25 unit tests passing across TASK-01, TASK-02, TASK-03 requirements --- src/main/task-quality-gates.ts | 160 +++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 src/main/task-quality-gates.ts diff --git a/src/main/task-quality-gates.ts b/src/main/task-quality-gates.ts new file mode 100644 index 00000000..090873a4 --- /dev/null +++ b/src/main/task-quality-gates.ts @@ -0,0 +1,160 @@ +/** + * Task Quality Gates — pure validation functions for task creation. + * + * Called by IPC handlers, API server, and automation service to enforce + * minimum description quality, acceptance criteria, and capability matching. + */ + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export interface TaskValidationResult { + valid: boolean; + errors: string[]; + warnings: string[]; +} + +export interface CapabilityMatchResult { + matched: boolean; + warning?: string; +} + +/** Minimal DB interface for capability matching — keeps the function testable with mocks. */ +export interface CapabilityMatchDb { + getAgent(id: string): { name: string; capabilities: string }; + listAgentSkillAssignments(id: string): string[]; + getCompanySkill(companyId: string, skillId: string): { name: string; description: string | null; slug: string } | null; +} + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +const MIN_DESCRIPTION_LENGTH = 50; + +export const ACCEPTANCE_CRITERIA_KEYWORDS: readonly string[] = [ + "acceptance criteria", + "done when", + "success criteria", + "definition of done", + "requirements:", + "must:", + "should:", + "- [ ]", + "- [x]", + "deliverable:", + "expected outcome", + "exit criteria", +]; + +// --------------------------------------------------------------------------- +// checkAcceptanceCriteria +// --------------------------------------------------------------------------- + +export function checkAcceptanceCriteria(description: string): { hasCriteria: boolean; suggestion: string } { + const lower = description.toLowerCase(); + const found = ACCEPTANCE_CRITERIA_KEYWORDS.some((kw) => lower.includes(kw)); + + if (found) { + return { hasCriteria: true, suggestion: "" }; + } + + return { + hasCriteria: false, + suggestion: + 'Task description should include acceptance criteria. Add a "Done when:" or "Acceptance criteria:" section, or use checkbox items (- [ ]) to define success conditions.', + }; +} + +// --------------------------------------------------------------------------- +// validateTaskCreation +// --------------------------------------------------------------------------- + +export function validateTaskCreation( + input: { title: string; description: string }, + isHeartbeat: boolean, +): TaskValidationResult { + // Heartbeat tasks bypass all validation + if (isHeartbeat) { + return { valid: true, errors: [], warnings: [] }; + } + + const errors: string[] = []; + const warnings: string[] = []; + + // TASK-01: Minimum description length + if (input.description.length < MIN_DESCRIPTION_LENGTH) { + errors.push( + `Task description must be at least ${MIN_DESCRIPTION_LENGTH} characters (currently ${input.description.length}). Add details about what needs to be done, expected behavior, and any constraints.`, + ); + } + + // TASK-02: Acceptance criteria detection (only check if length passes) + if (input.description.length >= MIN_DESCRIPTION_LENGTH) { + const criteria = checkAcceptanceCriteria(input.description); + if (!criteria.hasCriteria) { + errors.push(criteria.suggestion); + } + } + + return { valid: errors.length === 0, errors, warnings }; +} + +// --------------------------------------------------------------------------- +// checkCapabilityMatch +// --------------------------------------------------------------------------- + +/** Tokenize text into words >3 chars, same pattern as database.ts autoAssignTask */ +function tokenize(text: string): string[] { + return text.toLowerCase().split(/\W+/).filter((w) => w.length > 3); +} + +export function checkCapabilityMatch( + db: CapabilityMatchDb, + agentId: string, + taskDescription: string, + companyId: string, +): CapabilityMatchResult { + const taskWords = tokenize(taskDescription); + + // Nothing to match against — task description too short/vague + if (taskWords.length === 0) { + return { matched: true }; + } + + const agent = db.getAgent(agentId); + const capWords = tokenize(agent.capabilities); + const capabilityOverlap = taskWords.filter((w) => capWords.includes(w)).length; + + // Check skill assignments for additional matching (graceful degradation) + let skillOverlap = 0; + try { + const skillIds = db.listAgentSkillAssignments(agentId); + for (const skillId of skillIds) { + const skill = db.getCompanySkill(companyId, skillId); + if (skill) { + const skillWords = tokenize(`${skill.name} ${skill.description ?? ""} ${skill.slug}`); + skillOverlap += taskWords.filter((w) => skillWords.includes(w)).length; + } + } + } catch { + // Skill system may not be available — graceful degradation per D-07 + } + + // Direct capability or skill overlap found + if (capabilityOverlap > 0 || skillOverlap > 0) { + return { matched: true }; + } + + // No capabilities defined at all — graceful degradation (Pitfall 4) + if (capWords.length === 0 && skillOverlap === 0) { + return { matched: true }; + } + + // Agent has capabilities but no overlap — warn + return { + matched: false, + warning: `Agent "${agent.name}" may lack skills for this task. Their capabilities (${agent.capabilities.slice(0, 80)}...) have no overlap with the task description. Consider reassigning.`, + }; +}