@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
package/src/prd/parser.ts
CHANGED
|
@@ -6,24 +6,37 @@
|
|
|
6
6
|
|
|
7
7
|
import { PRDSchema, type PRD, type UserStory, type ExecutionHistory, type EscalationAttempt } from "./types";
|
|
8
8
|
import type { Mode, HarnessName } from "../config/schema";
|
|
9
|
+
import { validateCriterion, type FilteredCriterion } from "./validator";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parse warning for filtered criteria
|
|
13
|
+
*/
|
|
14
|
+
export interface ParseWarning {
|
|
15
|
+
type: "filtered_criterion" | "format_normalized" | "dependency_format";
|
|
16
|
+
storyId: string;
|
|
17
|
+
text: string;
|
|
18
|
+
reason: string;
|
|
19
|
+
suggestion?: string;
|
|
20
|
+
line?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Extended parse result with warnings
|
|
25
|
+
*/
|
|
26
|
+
export interface ParseResult {
|
|
27
|
+
prd: Partial<PRD>;
|
|
28
|
+
warnings: ParseWarning[];
|
|
29
|
+
filteredCriteria: FilteredCriterion[];
|
|
30
|
+
}
|
|
9
31
|
|
|
10
32
|
/**
|
|
11
33
|
* Check if a criterion line is valid (not a file path, divider, etc.)
|
|
34
|
+
*
|
|
35
|
+
* Now uses the validator module for consistent filtering logic.
|
|
12
36
|
*/
|
|
13
37
|
function isValidCriterion(text: string): boolean {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
// Skip if it's just a section marker
|
|
19
|
-
if (text.startsWith("**")) {
|
|
20
|
-
return false;
|
|
21
|
-
}
|
|
22
|
-
// Skip if it's empty or too short
|
|
23
|
-
if (text.length < 3) {
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
return true;
|
|
38
|
+
const result = validateCriterion(text);
|
|
39
|
+
return result.valid;
|
|
27
40
|
}
|
|
28
41
|
|
|
29
42
|
/**
|
|
@@ -248,6 +261,285 @@ export function parsePRDMarkdown(content: string): Partial<PRD> {
|
|
|
248
261
|
return prd;
|
|
249
262
|
}
|
|
250
263
|
|
|
264
|
+
/**
|
|
265
|
+
* Parse a PRD markdown file with detailed warnings about filtered content
|
|
266
|
+
*
|
|
267
|
+
* This version tracks all filtering decisions and provides actionable feedback.
|
|
268
|
+
*/
|
|
269
|
+
export function parsePRDMarkdownWithWarnings(content: string): ParseResult {
|
|
270
|
+
const lines = content.split("\n");
|
|
271
|
+
const prd: Partial<PRD> = {
|
|
272
|
+
userStories: [],
|
|
273
|
+
};
|
|
274
|
+
const warnings: ParseWarning[] = [];
|
|
275
|
+
const filteredCriteria: FilteredCriterion[] = [];
|
|
276
|
+
|
|
277
|
+
let currentSection = "";
|
|
278
|
+
let currentStory: Partial<UserStory> | null = null;
|
|
279
|
+
let currentStoryId = "";
|
|
280
|
+
let storyCount = 0;
|
|
281
|
+
let inAcceptanceCriteria = false;
|
|
282
|
+
let descriptionLines: string[] = [];
|
|
283
|
+
|
|
284
|
+
for (let i = 0; i < lines.length; i++) {
|
|
285
|
+
const line = lines[i];
|
|
286
|
+
const trimmed = line.trim();
|
|
287
|
+
const lineNum = i + 1;
|
|
288
|
+
|
|
289
|
+
// Parse title (# PRD: Title or # Title)
|
|
290
|
+
if (trimmed.startsWith("# ") && !trimmed.startsWith("## ") && !trimmed.startsWith("### ")) {
|
|
291
|
+
prd.project = trimmed.replace(/^#\s*(PRD:\s*)?/, "").trim();
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Parse routing preference line
|
|
296
|
+
if (trimmed.match(/^(\*\*Routing Preference\*\*|Routing Preference):/i)) {
|
|
297
|
+
const raw = trimmed.replace(/^(\*\*Routing Preference\*\*|Routing Preference):/i, "").trim();
|
|
298
|
+
const lower = raw.toLowerCase();
|
|
299
|
+
const modeMatch = lower.match(/\b(free|cheap|good|genius)\b/);
|
|
300
|
+
const allowFreeMatch = lower.match(/allow\s+free:\s*(yes|no)/);
|
|
301
|
+
const harnessMatch = lower.match(/\b(claude|amp|opencode|codex|droid|gemini)\b/);
|
|
302
|
+
const modelMatch = raw.match(/\/([^\s]+)/);
|
|
303
|
+
|
|
304
|
+
prd.routingPreference = {
|
|
305
|
+
raw,
|
|
306
|
+
type: lower.includes("auto") ? "auto" : harnessMatch ? "harness" : undefined,
|
|
307
|
+
mode: modeMatch ? (modeMatch[1] as Mode) : undefined,
|
|
308
|
+
allowFree: allowFreeMatch ? allowFreeMatch[1] === "yes" : undefined,
|
|
309
|
+
harness: harnessMatch ? (harnessMatch[1] as HarnessName) : undefined,
|
|
310
|
+
model: modelMatch ? modelMatch[1].replace(/[,)]$/, "") : undefined,
|
|
311
|
+
};
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Parse section headers (## Section)
|
|
316
|
+
if (trimmed.startsWith("## ")) {
|
|
317
|
+
if (currentStory && currentStory.id) {
|
|
318
|
+
if (descriptionLines.length > 0 && !currentStory.description) {
|
|
319
|
+
currentStory.description = descriptionLines.join(" ").trim();
|
|
320
|
+
}
|
|
321
|
+
prd.userStories!.push(currentStory as UserStory);
|
|
322
|
+
currentStory = null;
|
|
323
|
+
descriptionLines = [];
|
|
324
|
+
}
|
|
325
|
+
currentSection = trimmed.replace("## ", "").toLowerCase();
|
|
326
|
+
inAcceptanceCriteria = false;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Parse user stories
|
|
331
|
+
const storyMatch = trimmed.match(/^###\s+(?:US-(\d+)|Story\s+(\d+)|(\d+)\.?)\s*:?\s*(.*)$/i);
|
|
332
|
+
if (storyMatch) {
|
|
333
|
+
if (currentStory && currentStory.id) {
|
|
334
|
+
if (descriptionLines.length > 0 && !currentStory.description) {
|
|
335
|
+
currentStory.description = descriptionLines.join(" ").trim();
|
|
336
|
+
}
|
|
337
|
+
prd.userStories!.push(currentStory as UserStory);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
storyCount++;
|
|
341
|
+
const storyNum = storyMatch[1] || storyMatch[2] || storyMatch[3] || String(storyCount);
|
|
342
|
+
currentStoryId = `US-${storyNum.padStart(3, "0")}`;
|
|
343
|
+
|
|
344
|
+
// Track format normalization
|
|
345
|
+
if (storyMatch[2]) {
|
|
346
|
+
warnings.push({
|
|
347
|
+
type: "format_normalized",
|
|
348
|
+
storyId: currentStoryId,
|
|
349
|
+
text: trimmed,
|
|
350
|
+
reason: `"Story ${storyMatch[2]}" format normalized to "${currentStoryId}"`,
|
|
351
|
+
line: lineNum,
|
|
352
|
+
});
|
|
353
|
+
} else if (storyMatch[3]) {
|
|
354
|
+
warnings.push({
|
|
355
|
+
type: "format_normalized",
|
|
356
|
+
storyId: currentStoryId,
|
|
357
|
+
text: trimmed,
|
|
358
|
+
reason: `Numbered format normalized to "${currentStoryId}"`,
|
|
359
|
+
line: lineNum,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
currentStory = {
|
|
364
|
+
id: currentStoryId,
|
|
365
|
+
title: storyMatch[4]?.trim() || "",
|
|
366
|
+
description: "",
|
|
367
|
+
acceptanceCriteria: [],
|
|
368
|
+
priority: storyCount,
|
|
369
|
+
passes: false,
|
|
370
|
+
notes: "",
|
|
371
|
+
dependencies: undefined,
|
|
372
|
+
parallel: undefined,
|
|
373
|
+
phase: undefined,
|
|
374
|
+
};
|
|
375
|
+
inAcceptanceCriteria = false;
|
|
376
|
+
descriptionLines = [];
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Check for acceptance criteria section header
|
|
381
|
+
if (currentStory && trimmed.match(/^\*\*Acceptance Criteria:?\*\*$/i)) {
|
|
382
|
+
inAcceptanceCriteria = true;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Parse story description
|
|
387
|
+
if (currentStory && trimmed.startsWith("**Description:**")) {
|
|
388
|
+
currentStory.description = trimmed.replace("**Description:**", "").trim();
|
|
389
|
+
inAcceptanceCriteria = false;
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Parse dependencies with format warnings
|
|
394
|
+
if (currentStory && trimmed.match(/^\*\*Dependencies:?\*\*/i)) {
|
|
395
|
+
const depsText = trimmed.replace(/^\*\*Dependencies:?\*\*/i, "").trim();
|
|
396
|
+
const deps = depsText
|
|
397
|
+
.split(/[,;]/)
|
|
398
|
+
.map((d) => {
|
|
399
|
+
// Check for underscore format and warn
|
|
400
|
+
const underscoreMatch = d.match(/US_(\d+)/i);
|
|
401
|
+
if (underscoreMatch) {
|
|
402
|
+
const normalized = `US-${underscoreMatch[1].padStart(3, "0")}`;
|
|
403
|
+
warnings.push({
|
|
404
|
+
type: "dependency_format",
|
|
405
|
+
storyId: currentStoryId,
|
|
406
|
+
text: d.trim(),
|
|
407
|
+
reason: `Dependency uses underscore format instead of dash`,
|
|
408
|
+
suggestion: `Use "${normalized}" instead of "${d.trim()}"`,
|
|
409
|
+
line: lineNum,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const match = d.match(/US[-_]?(\d+)/i);
|
|
414
|
+
return match ? `US-${match[1].padStart(3, "0")}` : null;
|
|
415
|
+
})
|
|
416
|
+
.filter((d): d is string => d !== null);
|
|
417
|
+
if (deps.length > 0) {
|
|
418
|
+
currentStory.dependencies = deps;
|
|
419
|
+
}
|
|
420
|
+
inAcceptanceCriteria = false;
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Parse parallel flag
|
|
425
|
+
if (currentStory && trimmed.match(/^\*\*Parallel:?\*\*/i)) {
|
|
426
|
+
const value = trimmed.replace(/^\*\*Parallel:?\*\*/i, "").trim().toLowerCase();
|
|
427
|
+
currentStory.parallel = value === "true" || value === "yes";
|
|
428
|
+
inAcceptanceCriteria = false;
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Parse phase
|
|
433
|
+
if (currentStory && trimmed.match(/^\*\*Phase:?\*\*/i)) {
|
|
434
|
+
const phase = trimmed.replace(/^\*\*Phase:?\*\*/i, "").trim();
|
|
435
|
+
if (phase) {
|
|
436
|
+
currentStory.phase = phase;
|
|
437
|
+
}
|
|
438
|
+
inAcceptanceCriteria = false;
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Parse research flag
|
|
443
|
+
if (currentStory && trimmed.match(/^\*\*Research:?\*\*/i)) {
|
|
444
|
+
const value = trimmed.replace(/^\*\*Research:?\*\*/i, "").trim().toLowerCase();
|
|
445
|
+
currentStory.research = value === "true" || value === "yes";
|
|
446
|
+
inAcceptanceCriteria = false;
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Check for section headers that end acceptance criteria
|
|
451
|
+
if (currentStory && trimmed.match(/^\*\*(Files|Note|Technical|Design)/i)) {
|
|
452
|
+
inAcceptanceCriteria = false;
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Parse acceptance criteria with filtering feedback
|
|
457
|
+
if (currentStory && trimmed.startsWith("-")) {
|
|
458
|
+
if (trimmed.match(/^-+$/)) {
|
|
459
|
+
inAcceptanceCriteria = false;
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (trimmed.match(/^-\s*\[.\]/)) {
|
|
464
|
+
const criterion = trimmed.replace(/^-\s*\[.\]\s*/, "").trim();
|
|
465
|
+
if (criterion) {
|
|
466
|
+
const validation = validateCriterion(criterion);
|
|
467
|
+
if (validation.valid) {
|
|
468
|
+
currentStory.acceptanceCriteria!.push(criterion);
|
|
469
|
+
} else {
|
|
470
|
+
filteredCriteria.push({
|
|
471
|
+
storyId: currentStoryId,
|
|
472
|
+
text: criterion,
|
|
473
|
+
reason: validation.reason!,
|
|
474
|
+
line: lineNum,
|
|
475
|
+
suggestion: validation.suggestion,
|
|
476
|
+
});
|
|
477
|
+
warnings.push({
|
|
478
|
+
type: "filtered_criterion",
|
|
479
|
+
storyId: currentStoryId,
|
|
480
|
+
text: criterion,
|
|
481
|
+
reason: validation.reason!,
|
|
482
|
+
suggestion: validation.suggestion,
|
|
483
|
+
line: lineNum,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
inAcceptanceCriteria = true;
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (inAcceptanceCriteria) {
|
|
492
|
+
const criterion = trimmed.replace(/^-\s*/, "").trim();
|
|
493
|
+
if (criterion) {
|
|
494
|
+
const validation = validateCriterion(criterion);
|
|
495
|
+
if (validation.valid) {
|
|
496
|
+
currentStory.acceptanceCriteria!.push(criterion);
|
|
497
|
+
} else {
|
|
498
|
+
filteredCriteria.push({
|
|
499
|
+
storyId: currentStoryId,
|
|
500
|
+
text: criterion,
|
|
501
|
+
reason: validation.reason!,
|
|
502
|
+
line: lineNum,
|
|
503
|
+
suggestion: validation.suggestion,
|
|
504
|
+
});
|
|
505
|
+
warnings.push({
|
|
506
|
+
type: "filtered_criterion",
|
|
507
|
+
storyId: currentStoryId,
|
|
508
|
+
text: criterion,
|
|
509
|
+
reason: validation.reason!,
|
|
510
|
+
suggestion: validation.suggestion,
|
|
511
|
+
line: lineNum,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Collect description lines
|
|
520
|
+
if (currentStory && !inAcceptanceCriteria && trimmed && !trimmed.startsWith("**") && !trimmed.startsWith("#")) {
|
|
521
|
+
descriptionLines.push(trimmed);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Parse description if in introduction/overview section
|
|
525
|
+
if ((currentSection === "introduction" || currentSection === "overview") && !currentStory) {
|
|
526
|
+
if (trimmed && !prd.description && !trimmed.startsWith("**")) {
|
|
527
|
+
prd.description = trimmed;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Save last story
|
|
533
|
+
if (currentStory && currentStory.id) {
|
|
534
|
+
if (descriptionLines.length > 0 && !currentStory.description) {
|
|
535
|
+
currentStory.description = descriptionLines.join(" ").trim();
|
|
536
|
+
}
|
|
537
|
+
prd.userStories!.push(currentStory as UserStory);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return { prd, warnings, filteredCriteria };
|
|
541
|
+
}
|
|
542
|
+
|
|
251
543
|
/**
|
|
252
544
|
* Generate branch name from project name
|
|
253
545
|
*/
|