@arvorco/relentless 0.4.5 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/constitution/templates/prompt.md +35 -45
- package/.claude/skills/convert/SKILL.md +55 -6
- package/.claude/skills/tasks/SKILL.md +26 -0
- package/CHANGELOG.md +41 -3
- package/bin/relentless.ts +124 -2
- package/package.json +1 -1
- package/relentless/prompt.md +32 -93
- package/src/agents/claude.ts +3 -3
- package/src/execution/context-builder.ts +417 -0
- package/src/execution/index.ts +1 -0
- package/src/execution/runner.ts +61 -16
- package/src/prd/index.ts +1 -0
- package/src/prd/parser.ts +305 -13
- package/src/prd/validator.ts +553 -0
- package/src/routing/registry.ts +6 -6
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRD Validator
|
|
3
|
+
*
|
|
4
|
+
* Validates tasks.md content before conversion to prd.json.
|
|
5
|
+
* Provides early detection of format issues with clear error messages.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Validation Types
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Severity level for validation issues
|
|
16
|
+
*/
|
|
17
|
+
export type ValidationSeverity = "error" | "warning" | "info";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validation issue with context
|
|
21
|
+
*/
|
|
22
|
+
export interface ValidationIssue {
|
|
23
|
+
severity: ValidationSeverity;
|
|
24
|
+
code: string;
|
|
25
|
+
message: string;
|
|
26
|
+
storyId?: string;
|
|
27
|
+
line?: number;
|
|
28
|
+
suggestion?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Filtered criterion with reason
|
|
33
|
+
*/
|
|
34
|
+
export interface FilteredCriterion {
|
|
35
|
+
storyId: string;
|
|
36
|
+
text: string;
|
|
37
|
+
reason: string;
|
|
38
|
+
line?: number;
|
|
39
|
+
suggestion?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Complete validation result
|
|
44
|
+
*/
|
|
45
|
+
export interface ValidationResult {
|
|
46
|
+
valid: boolean;
|
|
47
|
+
errors: ValidationIssue[];
|
|
48
|
+
warnings: ValidationIssue[];
|
|
49
|
+
info: ValidationIssue[];
|
|
50
|
+
filteredCriteria: FilteredCriterion[];
|
|
51
|
+
summary: {
|
|
52
|
+
totalStories: number;
|
|
53
|
+
totalCriteria: number;
|
|
54
|
+
filteredCriteriaCount: number;
|
|
55
|
+
storiesWithNoCriteria: string[];
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// Validation Schemas
|
|
61
|
+
// ============================================================================
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Schema for story ID format
|
|
65
|
+
*/
|
|
66
|
+
export const StoryIdSchema = z.string().regex(/^US-\d{3}$/, {
|
|
67
|
+
message: "Story ID must be in US-XXX format (e.g., US-001)",
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// Criterion Validation
|
|
72
|
+
// ============================================================================
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Result of validating a criterion
|
|
76
|
+
*/
|
|
77
|
+
export interface CriterionValidation {
|
|
78
|
+
valid: boolean;
|
|
79
|
+
reason?: string;
|
|
80
|
+
suggestion?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if a criterion line is valid with detailed feedback
|
|
85
|
+
*
|
|
86
|
+
* Returns validation result with reason and suggestion for invalid criteria.
|
|
87
|
+
*/
|
|
88
|
+
export function validateCriterion(text: string): CriterionValidation {
|
|
89
|
+
// Skip empty or very short lines
|
|
90
|
+
if (!text || text.length < 3) {
|
|
91
|
+
return {
|
|
92
|
+
valid: false,
|
|
93
|
+
reason: "Criterion too short (less than 3 characters)",
|
|
94
|
+
suggestion: "Add more context to describe what should be verified",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Skip pure file paths like `src/file.ts` (standalone, not in a sentence)
|
|
99
|
+
// But allow file paths WITH context like "`src/file.ts` contains Zod schemas"
|
|
100
|
+
if (text.match(/^`[^`]+\.(ts|tsx|js|jsx|css|json|md|py|go|rs|java|kt|swift|rb)`$/)) {
|
|
101
|
+
return {
|
|
102
|
+
valid: false,
|
|
103
|
+
reason: "Standalone file path without context",
|
|
104
|
+
suggestion: `Add context: "${text} contains [description]" or "${text} is updated with [changes]"`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Skip pure section markers like **Files:** or **Note:**
|
|
109
|
+
// But allow labeled criteria like **Important:** User can log in
|
|
110
|
+
if (text.match(/^\*\*[^*:]+:\*\*\s*$/)) {
|
|
111
|
+
return {
|
|
112
|
+
valid: false,
|
|
113
|
+
reason: "Section marker without content",
|
|
114
|
+
suggestion: "Either add content after the marker or remove from acceptance criteria",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Skip dividers
|
|
119
|
+
if (text.match(/^[-=]{3,}$/)) {
|
|
120
|
+
return {
|
|
121
|
+
valid: false,
|
|
122
|
+
reason: "Line divider, not an acceptance criterion",
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { valid: true };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ============================================================================
|
|
130
|
+
// Story Validation
|
|
131
|
+
// ============================================================================
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Extract story ID from a markdown header line
|
|
135
|
+
*/
|
|
136
|
+
export function parseStoryId(line: string): { id: string | null; format: "standard" | "story" | "numbered" | null } {
|
|
137
|
+
// ### US-001: Title
|
|
138
|
+
const usMatch = line.match(/^###\s+US-(\d+)\s*:?\s*/i);
|
|
139
|
+
if (usMatch) {
|
|
140
|
+
return { id: `US-${usMatch[1].padStart(3, "0")}`, format: "standard" };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ### Story 1: Title
|
|
144
|
+
const storyMatch = line.match(/^###\s+Story\s+(\d+)\s*:?\s*/i);
|
|
145
|
+
if (storyMatch) {
|
|
146
|
+
return { id: `US-${storyMatch[1].padStart(3, "0")}`, format: "story" };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ### 1. Title or ### 1: Title
|
|
150
|
+
const numberedMatch = line.match(/^###\s+(\d+)\.?\s*:?\s*/);
|
|
151
|
+
if (numberedMatch) {
|
|
152
|
+
return { id: `US-${numberedMatch[1].padStart(3, "0")}`, format: "numbered" };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return { id: null, format: null };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Parse dependency line and extract referenced story IDs
|
|
160
|
+
*/
|
|
161
|
+
export function parseDependencies(line: string): { ids: string[]; issues: ValidationIssue[] } {
|
|
162
|
+
const issues: ValidationIssue[] = [];
|
|
163
|
+
const ids: string[] = [];
|
|
164
|
+
|
|
165
|
+
// Extract all US-XXX references
|
|
166
|
+
const usMatches = line.matchAll(/US[-_]?(\d+)/gi);
|
|
167
|
+
for (const match of usMatches) {
|
|
168
|
+
const fullMatch = match[0];
|
|
169
|
+
const number = match[1].padStart(3, "0");
|
|
170
|
+
const normalized = `US-${number}`;
|
|
171
|
+
|
|
172
|
+
// Check for underscore format
|
|
173
|
+
if (fullMatch.includes("_")) {
|
|
174
|
+
issues.push({
|
|
175
|
+
severity: "warning",
|
|
176
|
+
code: "DEPENDENCY_FORMAT",
|
|
177
|
+
message: `Dependency "${fullMatch}" uses underscore instead of dash`,
|
|
178
|
+
suggestion: `Use "${normalized}" instead of "${fullMatch}"`,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
ids.push(normalized);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { ids, issues };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ============================================================================
|
|
189
|
+
// Main Validation Function
|
|
190
|
+
// ============================================================================
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Validate tasks.md content before conversion
|
|
194
|
+
*
|
|
195
|
+
* Checks for:
|
|
196
|
+
* - Story ID format and uniqueness
|
|
197
|
+
* - Dependency validity (no circular, no missing)
|
|
198
|
+
* - Acceptance criteria quality (warns on filtered criteria)
|
|
199
|
+
* - Common format issues
|
|
200
|
+
*/
|
|
201
|
+
export function validateTasksMarkdown(content: string): ValidationResult {
|
|
202
|
+
const lines = content.split("\n");
|
|
203
|
+
const result: ValidationResult = {
|
|
204
|
+
valid: true,
|
|
205
|
+
errors: [],
|
|
206
|
+
warnings: [],
|
|
207
|
+
info: [],
|
|
208
|
+
filteredCriteria: [],
|
|
209
|
+
summary: {
|
|
210
|
+
totalStories: 0,
|
|
211
|
+
totalCriteria: 0,
|
|
212
|
+
filteredCriteriaCount: 0,
|
|
213
|
+
storiesWithNoCriteria: [],
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// Track state
|
|
218
|
+
const storyIds = new Map<string, number>(); // id -> line number
|
|
219
|
+
const storyDependencies = new Map<string, string[]>(); // id -> dependency ids
|
|
220
|
+
const storyCriteriaCount = new Map<string, number>(); // id -> count of valid criteria
|
|
221
|
+
let currentStoryId: string | null = null;
|
|
222
|
+
let inAcceptanceCriteria = false;
|
|
223
|
+
|
|
224
|
+
for (let i = 0; i < lines.length; i++) {
|
|
225
|
+
const line = lines[i];
|
|
226
|
+
const trimmed = line.trim();
|
|
227
|
+
const lineNum = i + 1; // 1-based line numbers for human readability
|
|
228
|
+
|
|
229
|
+
// Check for story header
|
|
230
|
+
const storyParsed = parseStoryId(trimmed);
|
|
231
|
+
if (storyParsed.id) {
|
|
232
|
+
// If we have a previous story, check if it had criteria
|
|
233
|
+
if (currentStoryId && (storyCriteriaCount.get(currentStoryId) ?? 0) === 0) {
|
|
234
|
+
result.summary.storiesWithNoCriteria.push(currentStoryId);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
currentStoryId = storyParsed.id;
|
|
238
|
+
inAcceptanceCriteria = false;
|
|
239
|
+
storyCriteriaCount.set(currentStoryId, 0);
|
|
240
|
+
|
|
241
|
+
// Check for non-standard format
|
|
242
|
+
if (storyParsed.format === "story") {
|
|
243
|
+
result.info.push({
|
|
244
|
+
severity: "info",
|
|
245
|
+
code: "STORY_FORMAT",
|
|
246
|
+
message: `Line ${lineNum}: "Story X" format will be normalized to "${currentStoryId}"`,
|
|
247
|
+
storyId: currentStoryId,
|
|
248
|
+
line: lineNum,
|
|
249
|
+
});
|
|
250
|
+
} else if (storyParsed.format === "numbered") {
|
|
251
|
+
result.info.push({
|
|
252
|
+
severity: "info",
|
|
253
|
+
code: "STORY_FORMAT",
|
|
254
|
+
message: `Line ${lineNum}: Numbered format will be normalized to "${currentStoryId}"`,
|
|
255
|
+
storyId: currentStoryId,
|
|
256
|
+
line: lineNum,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Check for duplicate story ID
|
|
261
|
+
if (storyIds.has(currentStoryId)) {
|
|
262
|
+
result.errors.push({
|
|
263
|
+
severity: "error",
|
|
264
|
+
code: "DUPLICATE_STORY_ID",
|
|
265
|
+
message: `Duplicate story ID "${currentStoryId}" at line ${lineNum} (first defined at line ${storyIds.get(currentStoryId)})`,
|
|
266
|
+
storyId: currentStoryId,
|
|
267
|
+
line: lineNum,
|
|
268
|
+
suggestion: `Use a unique ID like US-${String(storyIds.size + 1).padStart(3, "0")}`,
|
|
269
|
+
});
|
|
270
|
+
result.valid = false;
|
|
271
|
+
} else {
|
|
272
|
+
storyIds.set(currentStoryId, lineNum);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
result.summary.totalStories++;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Check for acceptance criteria section
|
|
280
|
+
if (currentStoryId && trimmed.match(/^\*\*Acceptance Criteria:?\*\*$/i)) {
|
|
281
|
+
inAcceptanceCriteria = true;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Check for dependency line
|
|
286
|
+
if (currentStoryId && trimmed.match(/^\*\*Dependencies?:?\*\*/i)) {
|
|
287
|
+
const { ids, issues } = parseDependencies(trimmed);
|
|
288
|
+
storyDependencies.set(currentStoryId, ids);
|
|
289
|
+
|
|
290
|
+
for (const issue of issues) {
|
|
291
|
+
issue.storyId = currentStoryId;
|
|
292
|
+
issue.line = lineNum;
|
|
293
|
+
result.warnings.push(issue);
|
|
294
|
+
}
|
|
295
|
+
inAcceptanceCriteria = false;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Check for section headers that end acceptance criteria
|
|
300
|
+
if (currentStoryId && trimmed.match(/^\*\*(Files|Note|Technical|Design|Phase|Priority|Parallel|Research)/i)) {
|
|
301
|
+
inAcceptanceCriteria = false;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Check for section header (##) ending current story
|
|
306
|
+
if (trimmed.startsWith("## ")) {
|
|
307
|
+
if (currentStoryId && (storyCriteriaCount.get(currentStoryId) ?? 0) === 0) {
|
|
308
|
+
result.summary.storiesWithNoCriteria.push(currentStoryId);
|
|
309
|
+
}
|
|
310
|
+
currentStoryId = null;
|
|
311
|
+
inAcceptanceCriteria = false;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Parse acceptance criteria
|
|
316
|
+
if (currentStoryId && trimmed.startsWith("-")) {
|
|
317
|
+
// Skip dividers
|
|
318
|
+
if (trimmed.match(/^-+$/)) {
|
|
319
|
+
inAcceptanceCriteria = false;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Extract criterion text
|
|
324
|
+
let criterionText = "";
|
|
325
|
+
if (trimmed.match(/^-\s*\[.\]/)) {
|
|
326
|
+
criterionText = trimmed.replace(/^-\s*\[.\]\s*/, "").trim();
|
|
327
|
+
inAcceptanceCriteria = true;
|
|
328
|
+
} else if (inAcceptanceCriteria) {
|
|
329
|
+
criterionText = trimmed.replace(/^-\s*/, "").trim();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (criterionText) {
|
|
333
|
+
result.summary.totalCriteria++;
|
|
334
|
+
|
|
335
|
+
const validation = validateCriterion(criterionText);
|
|
336
|
+
if (!validation.valid) {
|
|
337
|
+
result.filteredCriteria.push({
|
|
338
|
+
storyId: currentStoryId,
|
|
339
|
+
text: criterionText,
|
|
340
|
+
reason: validation.reason!,
|
|
341
|
+
line: lineNum,
|
|
342
|
+
suggestion: validation.suggestion,
|
|
343
|
+
});
|
|
344
|
+
result.summary.filteredCriteriaCount++;
|
|
345
|
+
} else {
|
|
346
|
+
const count = storyCriteriaCount.get(currentStoryId) ?? 0;
|
|
347
|
+
storyCriteriaCount.set(currentStoryId, count + 1);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Check last story for criteria
|
|
354
|
+
if (currentStoryId && (storyCriteriaCount.get(currentStoryId) ?? 0) === 0) {
|
|
355
|
+
result.summary.storiesWithNoCriteria.push(currentStoryId);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Validate dependencies
|
|
359
|
+
for (const [storyId, deps] of storyDependencies) {
|
|
360
|
+
for (const depId of deps) {
|
|
361
|
+
if (!storyIds.has(depId)) {
|
|
362
|
+
result.errors.push({
|
|
363
|
+
severity: "error",
|
|
364
|
+
code: "MISSING_DEPENDENCY",
|
|
365
|
+
message: `Story ${storyId} depends on non-existent story ${depId}`,
|
|
366
|
+
storyId,
|
|
367
|
+
suggestion: `Check if the dependency ID is correct or create ${depId}`,
|
|
368
|
+
});
|
|
369
|
+
result.valid = false;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Check for circular dependencies
|
|
375
|
+
const circularCheck = detectCircularDependencies(storyDependencies);
|
|
376
|
+
if (circularCheck.hasCircle) {
|
|
377
|
+
result.errors.push({
|
|
378
|
+
severity: "error",
|
|
379
|
+
code: "CIRCULAR_DEPENDENCY",
|
|
380
|
+
message: `Circular dependency detected: ${circularCheck.cycle!.join(" → ")}`,
|
|
381
|
+
suggestion: "Review dependencies and remove the circular reference",
|
|
382
|
+
});
|
|
383
|
+
result.valid = false;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Add warnings for stories with no acceptance criteria after filtering
|
|
387
|
+
for (const storyId of result.summary.storiesWithNoCriteria) {
|
|
388
|
+
result.warnings.push({
|
|
389
|
+
severity: "warning",
|
|
390
|
+
code: "NO_CRITERIA",
|
|
391
|
+
message: `Story ${storyId} has no valid acceptance criteria after filtering`,
|
|
392
|
+
storyId,
|
|
393
|
+
suggestion: "Add acceptance criteria that describe testable requirements",
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Add warnings for filtered criteria
|
|
398
|
+
if (result.filteredCriteria.length > 0) {
|
|
399
|
+
result.warnings.push({
|
|
400
|
+
severity: "warning",
|
|
401
|
+
code: "FILTERED_CRITERIA",
|
|
402
|
+
message: `${result.filteredCriteria.length} acceptance criteria will be filtered during conversion`,
|
|
403
|
+
suggestion: "Review the filtered criteria list and add context where needed",
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return result;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ============================================================================
|
|
411
|
+
// Circular Dependency Detection
|
|
412
|
+
// ============================================================================
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Detect circular dependencies using DFS
|
|
416
|
+
*/
|
|
417
|
+
function detectCircularDependencies(
|
|
418
|
+
dependencies: Map<string, string[]>
|
|
419
|
+
): { hasCircle: boolean; cycle?: string[] } {
|
|
420
|
+
const visited = new Set<string>();
|
|
421
|
+
const recursionStack = new Set<string>();
|
|
422
|
+
|
|
423
|
+
function dfs(storyId: string, path: string[]): string[] | null {
|
|
424
|
+
if (recursionStack.has(storyId)) {
|
|
425
|
+
// Found a cycle - return the cycle path
|
|
426
|
+
const cycleStart = path.indexOf(storyId);
|
|
427
|
+
return [...path.slice(cycleStart), storyId];
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (visited.has(storyId)) {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
visited.add(storyId);
|
|
435
|
+
recursionStack.add(storyId);
|
|
436
|
+
|
|
437
|
+
const deps = dependencies.get(storyId) ?? [];
|
|
438
|
+
for (const depId of deps) {
|
|
439
|
+
const cycle = dfs(depId, [...path, storyId]);
|
|
440
|
+
if (cycle) {
|
|
441
|
+
return cycle;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
recursionStack.delete(storyId);
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
for (const storyId of dependencies.keys()) {
|
|
450
|
+
const cycle = dfs(storyId, []);
|
|
451
|
+
if (cycle) {
|
|
452
|
+
return { hasCircle: true, cycle };
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return { hasCircle: false };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ============================================================================
|
|
460
|
+
// Formatting
|
|
461
|
+
// ============================================================================
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Format validation result as human-readable output
|
|
465
|
+
*/
|
|
466
|
+
export function formatValidationResult(result: ValidationResult): string {
|
|
467
|
+
const lines: string[] = [];
|
|
468
|
+
|
|
469
|
+
// Header
|
|
470
|
+
if (result.valid) {
|
|
471
|
+
lines.push("✅ Validation passed");
|
|
472
|
+
} else {
|
|
473
|
+
lines.push("❌ Validation failed");
|
|
474
|
+
}
|
|
475
|
+
lines.push("");
|
|
476
|
+
|
|
477
|
+
// Summary
|
|
478
|
+
lines.push(`Stories: ${result.summary.totalStories}`);
|
|
479
|
+
lines.push(`Criteria: ${result.summary.totalCriteria} total, ${result.summary.filteredCriteriaCount} filtered`);
|
|
480
|
+
lines.push("");
|
|
481
|
+
|
|
482
|
+
// Errors
|
|
483
|
+
if (result.errors.length > 0) {
|
|
484
|
+
lines.push(`ERRORS (${result.errors.length}):`);
|
|
485
|
+
for (const error of result.errors) {
|
|
486
|
+
const location = error.line ? ` (line ${error.line})` : "";
|
|
487
|
+
lines.push(` ❌ [${error.code}]${location}`);
|
|
488
|
+
lines.push(` ${error.message}`);
|
|
489
|
+
if (error.suggestion) {
|
|
490
|
+
lines.push(` 💡 ${error.suggestion}`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
lines.push("");
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Warnings
|
|
497
|
+
if (result.warnings.length > 0) {
|
|
498
|
+
lines.push(`WARNINGS (${result.warnings.length}):`);
|
|
499
|
+
for (const warning of result.warnings) {
|
|
500
|
+
const location = warning.line ? ` (line ${warning.line})` : "";
|
|
501
|
+
lines.push(` ⚠️ [${warning.code}]${location}`);
|
|
502
|
+
lines.push(` ${warning.message}`);
|
|
503
|
+
if (warning.suggestion) {
|
|
504
|
+
lines.push(` 💡 ${warning.suggestion}`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
lines.push("");
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Filtered criteria details (if any)
|
|
511
|
+
if (result.filteredCriteria.length > 0 && result.filteredCriteria.length <= 10) {
|
|
512
|
+
lines.push("FILTERED CRITERIA:");
|
|
513
|
+
for (const fc of result.filteredCriteria) {
|
|
514
|
+
const location = fc.line ? ` (line ${fc.line})` : "";
|
|
515
|
+
lines.push(` ${fc.storyId}${location}: "${fc.text}"`);
|
|
516
|
+
lines.push(` Reason: ${fc.reason}`);
|
|
517
|
+
if (fc.suggestion) {
|
|
518
|
+
lines.push(` 💡 ${fc.suggestion}`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
lines.push("");
|
|
522
|
+
} else if (result.filteredCriteria.length > 10) {
|
|
523
|
+
lines.push(`FILTERED CRITERIA: ${result.filteredCriteria.length} items (use --verbose to see all)`);
|
|
524
|
+
lines.push("");
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Stories with no criteria
|
|
528
|
+
if (result.summary.storiesWithNoCriteria.length > 0) {
|
|
529
|
+
lines.push("STORIES WITH NO VALID CRITERIA:");
|
|
530
|
+
for (const storyId of result.summary.storiesWithNoCriteria) {
|
|
531
|
+
lines.push(` ${storyId}`);
|
|
532
|
+
}
|
|
533
|
+
lines.push("");
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Info (only if there are no errors or warnings)
|
|
537
|
+
if (result.info.length > 0 && result.errors.length === 0 && result.warnings.length === 0) {
|
|
538
|
+
lines.push(`INFO (${result.info.length}):`);
|
|
539
|
+
for (const info of result.info) {
|
|
540
|
+
lines.push(` ℹ️ ${info.message}`);
|
|
541
|
+
}
|
|
542
|
+
lines.push("");
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return lines.join("\n");
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Format validation result as JSON
|
|
550
|
+
*/
|
|
551
|
+
export function formatValidationResultJSON(result: ValidationResult): string {
|
|
552
|
+
return JSON.stringify(result, null, 2);
|
|
553
|
+
}
|
package/src/routing/registry.ts
CHANGED
|
@@ -93,7 +93,7 @@ export const MODEL_REGISTRY: ModelProfile[] = [
|
|
|
93
93
|
strengths: ["code_review", "architecture", "debugging", "final_review", "complex_reasoning"],
|
|
94
94
|
limitations: ["expensive", "slower_start"],
|
|
95
95
|
cliFlag: "--model",
|
|
96
|
-
cliValue: "claude-opus-4-5
|
|
96
|
+
cliValue: "claude-opus-4-5",
|
|
97
97
|
},
|
|
98
98
|
{
|
|
99
99
|
id: "sonnet-4.5",
|
|
@@ -106,7 +106,7 @@ export const MODEL_REGISTRY: ModelProfile[] = [
|
|
|
106
106
|
strengths: ["frontend", "refactoring", "daily_coding", "balanced"],
|
|
107
107
|
limitations: [],
|
|
108
108
|
cliFlag: "--model",
|
|
109
|
-
cliValue: "claude-sonnet-4-5
|
|
109
|
+
cliValue: "claude-sonnet-4-5",
|
|
110
110
|
},
|
|
111
111
|
{
|
|
112
112
|
id: "haiku-4.5",
|
|
@@ -121,7 +121,7 @@ export const MODEL_REGISTRY: ModelProfile[] = [
|
|
|
121
121
|
strengths: ["prototyping", "scaffolding", "simple_tasks", "fast"],
|
|
122
122
|
limitations: ["less_reasoning"],
|
|
123
123
|
cliFlag: "--model",
|
|
124
|
-
cliValue: "claude-haiku-4-5
|
|
124
|
+
cliValue: "claude-haiku-4-5",
|
|
125
125
|
},
|
|
126
126
|
|
|
127
127
|
// ============== Codex (OpenAI) Models ==============
|
|
@@ -196,7 +196,7 @@ export const MODEL_REGISTRY: ModelProfile[] = [
|
|
|
196
196
|
strengths: ["architecture", "debugging", "complex_reasoning"],
|
|
197
197
|
limitations: ["expensive"],
|
|
198
198
|
cliFlag: "-m",
|
|
199
|
-
cliValue: "claude-opus-4-5
|
|
199
|
+
cliValue: "claude-opus-4-5",
|
|
200
200
|
},
|
|
201
201
|
{
|
|
202
202
|
id: "claude-sonnet-4-5-20250929",
|
|
@@ -209,7 +209,7 @@ export const MODEL_REGISTRY: ModelProfile[] = [
|
|
|
209
209
|
strengths: ["balanced", "daily_coding"],
|
|
210
210
|
limitations: [],
|
|
211
211
|
cliFlag: "-m",
|
|
212
|
-
cliValue: "claude-sonnet-4-5
|
|
212
|
+
cliValue: "claude-sonnet-4-5",
|
|
213
213
|
},
|
|
214
214
|
{
|
|
215
215
|
id: "claude-haiku-4-5-20251001",
|
|
@@ -222,7 +222,7 @@ export const MODEL_REGISTRY: ModelProfile[] = [
|
|
|
222
222
|
strengths: ["fast", "simple_tasks"],
|
|
223
223
|
limitations: ["less_reasoning"],
|
|
224
224
|
cliFlag: "-m",
|
|
225
|
-
cliValue: "claude-haiku-4-5
|
|
225
|
+
cliValue: "claude-haiku-4-5",
|
|
226
226
|
},
|
|
227
227
|
{
|
|
228
228
|
id: "gpt-5.2",
|