@arvorco/relentless 0.1.0
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/commands/relentless.analyze.md +20 -0
- package/.claude/commands/relentless.checklist.md +15 -0
- package/.claude/commands/relentless.clarify.md +19 -0
- package/.claude/commands/relentless.constitution.md +78 -0
- package/.claude/commands/relentless.implement.md +15 -0
- package/.claude/commands/relentless.plan.md +22 -0
- package/.claude/commands/relentless.plan.old.md +89 -0
- package/.claude/commands/relentless.specify.md +254 -0
- package/.claude/commands/relentless.tasks.md +25 -0
- package/.claude/commands/relentless.taskstoissues.md +15 -0
- package/.claude/settings.local.json +23 -0
- package/.claude/skills/analyze/SKILL.md +149 -0
- package/.claude/skills/checklist/SKILL.md +173 -0
- package/.claude/skills/checklist/templates/checklist-template.md +40 -0
- package/.claude/skills/clarify/SKILL.md +174 -0
- package/.claude/skills/constitution/SKILL.md +150 -0
- package/.claude/skills/constitution/templates/constitution-template.md +228 -0
- package/.claude/skills/implement/SKILL.md +141 -0
- package/.claude/skills/plan/SKILL.md +179 -0
- package/.claude/skills/plan/templates/plan-template.md +104 -0
- package/.claude/skills/prd/SKILL.md +242 -0
- package/.claude/skills/relentless/SKILL.md +265 -0
- package/.claude/skills/specify/SKILL.md +220 -0
- package/.claude/skills/specify/scripts/bash/check-prerequisites.sh +166 -0
- package/.claude/skills/specify/scripts/bash/common.sh +156 -0
- package/.claude/skills/specify/scripts/bash/create-new-feature.sh +305 -0
- package/.claude/skills/specify/scripts/bash/setup-plan.sh +61 -0
- package/.claude/skills/specify/scripts/bash/update-agent-context.sh +799 -0
- package/.claude/skills/specify/templates/spec-template.md +115 -0
- package/.claude/skills/tasks/SKILL.md +202 -0
- package/.claude/skills/tasks/templates/tasks-template.md +251 -0
- package/.claude/skills/taskstoissues/SKILL.md +97 -0
- package/.specify/memory/constitution.md +50 -0
- package/.specify/scripts/bash/check-prerequisites.sh +166 -0
- package/.specify/scripts/bash/common.sh +156 -0
- package/.specify/scripts/bash/create-new-feature.sh +297 -0
- package/.specify/scripts/bash/setup-plan.sh +61 -0
- package/.specify/scripts/bash/update-agent-context.sh +799 -0
- package/.specify/templates/agent-file-template.md +28 -0
- package/.specify/templates/checklist-template.md +40 -0
- package/.specify/templates/plan-template.md +104 -0
- package/.specify/templates/spec-template.md +115 -0
- package/.specify/templates/tasks-template.md +251 -0
- package/CHANGES_SUMMARY.md +255 -0
- package/CLAUDE.md +92 -0
- package/GEMINI_SETUP.md +256 -0
- package/LICENSE +21 -0
- package/README.md +1171 -0
- package/REFACTOR_SUMMARY.md +267 -0
- package/bin/relentless.ts +536 -0
- package/bun.lock +352 -0
- package/eslint.config.js +37 -0
- package/package.json +61 -0
- package/prd.json.example +64 -0
- package/prompt.md +108 -0
- package/ralph.sh +80 -0
- package/relentless/config.json +38 -0
- package/relentless/features/.gitkeep +0 -0
- package/relentless/features/ghsk-ideas/prd.json +229 -0
- package/relentless/features/ghsk-ideas/prd.md +191 -0
- package/relentless/features/ghsk-ideas/progress.txt +408 -0
- package/relentless/prompt.md +79 -0
- package/skills/checklist/SKILL.md +349 -0
- package/skills/clarify/SKILL.md +476 -0
- package/skills/prd/SKILL.md +242 -0
- package/skills/relentless/SKILL.md +268 -0
- package/skills/tasks/SKILL.md +577 -0
- package/src/agents/amp.ts +115 -0
- package/src/agents/claude.ts +185 -0
- package/src/agents/codex.ts +89 -0
- package/src/agents/droid.ts +90 -0
- package/src/agents/gemini.ts +109 -0
- package/src/agents/index.ts +16 -0
- package/src/agents/opencode.ts +88 -0
- package/src/agents/registry.ts +95 -0
- package/src/agents/types.ts +101 -0
- package/src/config/index.ts +8 -0
- package/src/config/loader.ts +237 -0
- package/src/config/schema.ts +115 -0
- package/src/execution/index.ts +8 -0
- package/src/execution/router.ts +49 -0
- package/src/execution/runner.ts +512 -0
- package/src/index.ts +11 -0
- package/src/init/index.ts +7 -0
- package/src/init/scaffolder.ts +377 -0
- package/src/prd/analyzer.ts +512 -0
- package/src/prd/index.ts +11 -0
- package/src/prd/issues.ts +249 -0
- package/src/prd/parser.ts +281 -0
- package/src/prd/progress.ts +198 -0
- package/src/prd/types.ts +170 -0
- package/src/tui/App.tsx +85 -0
- package/src/tui/TUIRunner.tsx +400 -0
- package/src/tui/components/AgentOutput.tsx +45 -0
- package/src/tui/components/AgentStatus.tsx +64 -0
- package/src/tui/components/CurrentStory.tsx +66 -0
- package/src/tui/components/Header.tsx +49 -0
- package/src/tui/components/ProgressBar.tsx +39 -0
- package/src/tui/components/StoryGrid.tsx +86 -0
- package/src/tui/hooks/useTUI.ts +147 -0
- package/src/tui/hooks/useTimer.ts +51 -0
- package/src/tui/index.tsx +17 -0
- package/src/tui/theme.ts +41 -0
- package/src/tui/types.ts +77 -0
- package/templates/constitution.md +228 -0
- package/templates/plan.md +273 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Issues Generator
|
|
3
|
+
*
|
|
4
|
+
* Converts user stories to GitHub issues via gh CLI
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PRD, UserStory } from "./types";
|
|
8
|
+
import { inferStoryType } from "./types";
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validate git remote matches expected repository
|
|
13
|
+
*/
|
|
14
|
+
async function validateGitRemote(): Promise<{ owner: string; repo: string }> {
|
|
15
|
+
const proc = Bun.spawn(["git", "remote", "get-url", "origin"]);
|
|
16
|
+
const output = await new Response(proc.stdout).text();
|
|
17
|
+
const exitCode = await proc.exited;
|
|
18
|
+
|
|
19
|
+
if (exitCode !== 0) {
|
|
20
|
+
throw new Error(`Failed to get git remote: exit code ${exitCode}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const url = output.trim();
|
|
24
|
+
|
|
25
|
+
// Parse GitHub remote URL
|
|
26
|
+
// Supports: git@github.com:owner/repo.git and https://github.com/owner/repo.git
|
|
27
|
+
const sshMatch = url.match(/git@github\.com:(.+?)\/(.+?)(?:\.git)?$/);
|
|
28
|
+
const httpsMatch = url.match(/https:\/\/github\.com\/(.+?)\/(.+?)(?:\.git)?$/);
|
|
29
|
+
|
|
30
|
+
const match = sshMatch || httpsMatch;
|
|
31
|
+
if (!match) {
|
|
32
|
+
throw new Error(`Invalid GitHub remote URL: ${url}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { owner: match[1], repo: match[2] };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Infer labels from story content
|
|
40
|
+
*/
|
|
41
|
+
function inferLabels(story: UserStory): string[] {
|
|
42
|
+
const labels: string[] = [];
|
|
43
|
+
const type = inferStoryType(story);
|
|
44
|
+
|
|
45
|
+
// Add primary type label
|
|
46
|
+
if (type !== "general") {
|
|
47
|
+
labels.push(type);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Add priority label
|
|
51
|
+
if (story.priority <= 3) {
|
|
52
|
+
labels.push("priority:high");
|
|
53
|
+
} else if (story.priority <= 6) {
|
|
54
|
+
labels.push("priority:medium");
|
|
55
|
+
} else {
|
|
56
|
+
labels.push("priority:low");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Add phase label if present
|
|
60
|
+
if (story.phase) {
|
|
61
|
+
labels.push(`phase:${story.phase.toLowerCase()}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Add research label if required
|
|
65
|
+
if (story.research) {
|
|
66
|
+
labels.push("research");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return labels;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Format story as GitHub issue body
|
|
74
|
+
*/
|
|
75
|
+
function formatIssueBody(story: UserStory): string {
|
|
76
|
+
const parts: string[] = [];
|
|
77
|
+
|
|
78
|
+
// Description
|
|
79
|
+
parts.push("## Description");
|
|
80
|
+
parts.push("");
|
|
81
|
+
parts.push(story.description);
|
|
82
|
+
parts.push("");
|
|
83
|
+
|
|
84
|
+
// Acceptance Criteria
|
|
85
|
+
parts.push("## Acceptance Criteria");
|
|
86
|
+
parts.push("");
|
|
87
|
+
for (const criterion of story.acceptanceCriteria) {
|
|
88
|
+
parts.push(`- [ ] ${criterion}`);
|
|
89
|
+
}
|
|
90
|
+
parts.push("");
|
|
91
|
+
|
|
92
|
+
// Dependencies (if any)
|
|
93
|
+
if (story.dependencies && story.dependencies.length > 0) {
|
|
94
|
+
parts.push("## Dependencies");
|
|
95
|
+
parts.push("");
|
|
96
|
+
parts.push("This story depends on:");
|
|
97
|
+
for (const depId of story.dependencies) {
|
|
98
|
+
parts.push(`- ${depId}`);
|
|
99
|
+
}
|
|
100
|
+
parts.push("");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Phase (if present)
|
|
104
|
+
if (story.phase) {
|
|
105
|
+
parts.push(`**Phase:** ${story.phase}`);
|
|
106
|
+
parts.push("");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Parallel (if applicable)
|
|
110
|
+
if (story.parallel) {
|
|
111
|
+
parts.push("**Can be executed in parallel** with other stories");
|
|
112
|
+
parts.push("");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Research (if required)
|
|
116
|
+
if (story.research) {
|
|
117
|
+
parts.push("⚠️ **Research Phase Required**: This story requires a research phase before implementation.");
|
|
118
|
+
parts.push("");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Notes (if present)
|
|
122
|
+
if (story.notes && story.notes.trim() !== "") {
|
|
123
|
+
parts.push("## Notes");
|
|
124
|
+
parts.push("");
|
|
125
|
+
parts.push(story.notes);
|
|
126
|
+
parts.push("");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return parts.join("\n");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Create a GitHub issue for a user story
|
|
134
|
+
*/
|
|
135
|
+
async function createIssue(story: UserStory, labels: string[]): Promise<string> {
|
|
136
|
+
const title = `[${story.id}] ${story.title}`;
|
|
137
|
+
const body = formatIssueBody(story);
|
|
138
|
+
|
|
139
|
+
// Build gh command
|
|
140
|
+
const args = [
|
|
141
|
+
"issue",
|
|
142
|
+
"create",
|
|
143
|
+
"--title",
|
|
144
|
+
title,
|
|
145
|
+
"--body",
|
|
146
|
+
body,
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
// Add labels
|
|
150
|
+
if (labels.length > 0) {
|
|
151
|
+
args.push("--label");
|
|
152
|
+
args.push(labels.join(","));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const proc = Bun.spawn(["gh", ...args]);
|
|
156
|
+
const output = await new Response(proc.stdout).text();
|
|
157
|
+
const stderr = await new Response(proc.stderr).text();
|
|
158
|
+
const exitCode = await proc.exited;
|
|
159
|
+
|
|
160
|
+
if (exitCode !== 0) {
|
|
161
|
+
throw new Error(`Failed to create issue for ${story.id}: ${stderr}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Extract issue URL from output (gh returns the URL)
|
|
165
|
+
return output.trim();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Check if gh CLI is installed
|
|
170
|
+
*/
|
|
171
|
+
async function checkGhCLI(): Promise<boolean> {
|
|
172
|
+
try {
|
|
173
|
+
const proc = Bun.spawn(["gh", "--version"]);
|
|
174
|
+
const exitCode = await proc.exited;
|
|
175
|
+
return exitCode === 0;
|
|
176
|
+
} catch (error) {
|
|
177
|
+
// Log unexpected errors (not just "command not found")
|
|
178
|
+
if (error instanceof Error && !error.message.includes("ENOENT")) {
|
|
179
|
+
console.warn(`Unexpected error checking gh CLI: ${error.message}`);
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Convert PRD user stories to GitHub issues
|
|
187
|
+
*/
|
|
188
|
+
export async function generateGitHubIssues(
|
|
189
|
+
prd: PRD,
|
|
190
|
+
options: {
|
|
191
|
+
dryRun?: boolean;
|
|
192
|
+
onlyIncomplete?: boolean;
|
|
193
|
+
} = {}
|
|
194
|
+
): Promise<{ created: number; skipped: number; errors: string[] }> {
|
|
195
|
+
const { dryRun = false, onlyIncomplete = true } = options;
|
|
196
|
+
|
|
197
|
+
// Check gh CLI
|
|
198
|
+
const hasGh = await checkGhCLI();
|
|
199
|
+
if (!hasGh) {
|
|
200
|
+
throw new Error("GitHub CLI (gh) is not installed. Install it from: https://cli.github.com/");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Validate git remote
|
|
204
|
+
console.log(chalk.dim("Validating git remote..."));
|
|
205
|
+
const remote = await validateGitRemote();
|
|
206
|
+
console.log(chalk.dim(`Repository: ${remote.owner}/${remote.repo}`));
|
|
207
|
+
|
|
208
|
+
// Filter stories
|
|
209
|
+
const stories = onlyIncomplete
|
|
210
|
+
? prd.userStories.filter((s) => !s.passes)
|
|
211
|
+
: prd.userStories;
|
|
212
|
+
|
|
213
|
+
if (stories.length === 0) {
|
|
214
|
+
console.log(chalk.yellow("No stories to convert to issues."));
|
|
215
|
+
return { created: 0, skipped: 0, errors: [] };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
console.log(chalk.dim(`Found ${stories.length} stories to convert\n`));
|
|
219
|
+
|
|
220
|
+
let created = 0;
|
|
221
|
+
let skipped = 0;
|
|
222
|
+
const errors: string[] = [];
|
|
223
|
+
|
|
224
|
+
for (const story of stories) {
|
|
225
|
+
const labels = inferLabels(story);
|
|
226
|
+
|
|
227
|
+
if (dryRun) {
|
|
228
|
+
console.log(chalk.cyan(`[DRY RUN] ${story.id}: ${story.title}`));
|
|
229
|
+
console.log(chalk.dim(` Labels: ${labels.join(", ")}`));
|
|
230
|
+
if (story.dependencies && story.dependencies.length > 0) {
|
|
231
|
+
console.log(chalk.dim(` Dependencies: ${story.dependencies.join(", ")}`));
|
|
232
|
+
}
|
|
233
|
+
created++;
|
|
234
|
+
} else {
|
|
235
|
+
try {
|
|
236
|
+
const issueUrl = await createIssue(story, labels);
|
|
237
|
+
console.log(chalk.green(`✓ ${story.id}: ${story.title}`));
|
|
238
|
+
console.log(chalk.dim(` ${issueUrl}`));
|
|
239
|
+
created++;
|
|
240
|
+
} catch (error) {
|
|
241
|
+
console.log(chalk.red(`✗ ${story.id}: ${story.title}`));
|
|
242
|
+
console.log(chalk.dim(` ${(error as Error).message}`));
|
|
243
|
+
errors.push(`${story.id}: ${(error as Error).message}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return { created, skipped, errors };
|
|
249
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRD Parser
|
|
3
|
+
*
|
|
4
|
+
* Parses PRD markdown files and converts them to prd.json format
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { PRDSchema, type PRD, type UserStory } from "./types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Check if a criterion line is valid (not a file path, divider, etc.)
|
|
11
|
+
*/
|
|
12
|
+
function isValidCriterion(text: string): boolean {
|
|
13
|
+
// Skip if it looks like a file path
|
|
14
|
+
if (text.match(/^`[^`]+\.(ts|tsx|js|jsx|css|json|md)`$/)) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
// Skip if it's just a section marker
|
|
18
|
+
if (text.startsWith("**")) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
// Skip if it's empty or too short
|
|
22
|
+
if (text.length < 3) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Parse a PRD markdown file into structured format
|
|
30
|
+
*
|
|
31
|
+
* Supports multiple story formats:
|
|
32
|
+
* - ### US-001: Title
|
|
33
|
+
* - ### Story 1: Title
|
|
34
|
+
* - ### 1. Title
|
|
35
|
+
*
|
|
36
|
+
* Supports multiple acceptance criteria formats:
|
|
37
|
+
* - [ ] Criterion
|
|
38
|
+
* - [x] Criterion
|
|
39
|
+
* - Criterion (plain bullet)
|
|
40
|
+
*/
|
|
41
|
+
export function parsePRDMarkdown(content: string): Partial<PRD> {
|
|
42
|
+
const lines = content.split("\n");
|
|
43
|
+
const prd: Partial<PRD> = {
|
|
44
|
+
userStories: [],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
let currentSection = "";
|
|
48
|
+
let currentStory: Partial<UserStory> | null = null;
|
|
49
|
+
let storyCount = 0;
|
|
50
|
+
let inAcceptanceCriteria = false;
|
|
51
|
+
let descriptionLines: string[] = [];
|
|
52
|
+
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
const trimmed = line.trim();
|
|
55
|
+
|
|
56
|
+
// Parse title (# PRD: Title or # Title)
|
|
57
|
+
if (trimmed.startsWith("# ") && !trimmed.startsWith("## ") && !trimmed.startsWith("### ")) {
|
|
58
|
+
prd.project = trimmed.replace(/^#\s*(PRD:\s*)?/, "").trim();
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Parse section headers (## Section)
|
|
63
|
+
if (trimmed.startsWith("## ")) {
|
|
64
|
+
currentSection = trimmed.replace("## ", "").toLowerCase();
|
|
65
|
+
inAcceptanceCriteria = false;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Parse user stories - multiple formats supported
|
|
70
|
+
// ### US-001: Title
|
|
71
|
+
// ### Story 1: Title
|
|
72
|
+
// ### 1. Title
|
|
73
|
+
const storyMatch = trimmed.match(/^###\s+(?:US-(\d+)|Story\s+(\d+)|(\d+)\.?)\s*:?\s*(.*)$/i);
|
|
74
|
+
if (storyMatch) {
|
|
75
|
+
// Save previous story
|
|
76
|
+
if (currentStory && currentStory.id) {
|
|
77
|
+
if (descriptionLines.length > 0 && !currentStory.description) {
|
|
78
|
+
currentStory.description = descriptionLines.join(" ").trim();
|
|
79
|
+
}
|
|
80
|
+
prd.userStories!.push(currentStory as UserStory);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
storyCount++;
|
|
84
|
+
const storyNum = storyMatch[1] || storyMatch[2] || storyMatch[3] || String(storyCount);
|
|
85
|
+
currentStory = {
|
|
86
|
+
id: `US-${storyNum.padStart(3, "0")}`,
|
|
87
|
+
title: storyMatch[4]?.trim() || "",
|
|
88
|
+
description: "",
|
|
89
|
+
acceptanceCriteria: [],
|
|
90
|
+
priority: storyCount,
|
|
91
|
+
passes: false,
|
|
92
|
+
notes: "",
|
|
93
|
+
dependencies: undefined,
|
|
94
|
+
parallel: undefined,
|
|
95
|
+
phase: undefined,
|
|
96
|
+
};
|
|
97
|
+
inAcceptanceCriteria = false;
|
|
98
|
+
descriptionLines = [];
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check for acceptance criteria section header
|
|
103
|
+
if (currentStory && trimmed.match(/^\*\*Acceptance Criteria:?\*\*$/i)) {
|
|
104
|
+
inAcceptanceCriteria = true;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Parse story description (single line after **Description:**)
|
|
109
|
+
if (currentStory && trimmed.startsWith("**Description:**")) {
|
|
110
|
+
currentStory.description = trimmed.replace("**Description:**", "").trim();
|
|
111
|
+
inAcceptanceCriteria = false;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Parse dependencies (Dependencies: US-001, US-002)
|
|
116
|
+
if (currentStory && trimmed.match(/^\*\*Dependencies:?\*\*/i)) {
|
|
117
|
+
const deps = trimmed
|
|
118
|
+
.replace(/^\*\*Dependencies:?\*\*/i, "")
|
|
119
|
+
.trim()
|
|
120
|
+
.split(/[,;]/)
|
|
121
|
+
.map((d) => d.trim())
|
|
122
|
+
.filter(Boolean);
|
|
123
|
+
if (deps.length > 0) {
|
|
124
|
+
currentStory.dependencies = deps;
|
|
125
|
+
}
|
|
126
|
+
inAcceptanceCriteria = false;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Parse parallel flag (Parallel: true/yes)
|
|
131
|
+
if (currentStory && trimmed.match(/^\*\*Parallel:?\*\*/i)) {
|
|
132
|
+
const value = trimmed.replace(/^\*\*Parallel:?\*\*/i, "").trim().toLowerCase();
|
|
133
|
+
currentStory.parallel = value === "true" || value === "yes";
|
|
134
|
+
inAcceptanceCriteria = false;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Parse phase (Phase: Setup)
|
|
139
|
+
if (currentStory && trimmed.match(/^\*\*Phase:?\*\*/i)) {
|
|
140
|
+
const phase = trimmed.replace(/^\*\*Phase:?\*\*/i, "").trim();
|
|
141
|
+
if (phase) {
|
|
142
|
+
currentStory.phase = phase;
|
|
143
|
+
}
|
|
144
|
+
inAcceptanceCriteria = false;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Parse research flag (Research: true/yes)
|
|
149
|
+
if (currentStory && trimmed.match(/^\*\*Research:?\*\*/i)) {
|
|
150
|
+
const value = trimmed.replace(/^\*\*Research:?\*\*/i, "").trim().toLowerCase();
|
|
151
|
+
currentStory.research = value === "true" || value === "yes";
|
|
152
|
+
inAcceptanceCriteria = false;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Check for section headers within story that end acceptance criteria
|
|
157
|
+
if (currentStory && trimmed.match(/^\*\*(Files|Note|Technical|Design)/i)) {
|
|
158
|
+
inAcceptanceCriteria = false;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Parse acceptance criteria - multiple formats
|
|
163
|
+
// - [ ] Criterion
|
|
164
|
+
// - [x] Criterion
|
|
165
|
+
// - Criterion (plain bullet, only in acceptance criteria section)
|
|
166
|
+
if (currentStory && trimmed.startsWith("-")) {
|
|
167
|
+
// Skip dividers like "---"
|
|
168
|
+
if (trimmed.match(/^-+$/)) {
|
|
169
|
+
inAcceptanceCriteria = false;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Checkbox format
|
|
174
|
+
if (trimmed.match(/^-\s*\[.\]/)) {
|
|
175
|
+
const criterion = trimmed.replace(/^-\s*\[.\]\s*/, "").trim();
|
|
176
|
+
if (criterion && isValidCriterion(criterion)) {
|
|
177
|
+
currentStory.acceptanceCriteria!.push(criterion);
|
|
178
|
+
}
|
|
179
|
+
inAcceptanceCriteria = true;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
// Plain bullet format (only if we're in acceptance criteria section)
|
|
183
|
+
if (inAcceptanceCriteria) {
|
|
184
|
+
const criterion = trimmed.replace(/^-\s*/, "").trim();
|
|
185
|
+
if (criterion && isValidCriterion(criterion)) {
|
|
186
|
+
currentStory.acceptanceCriteria!.push(criterion);
|
|
187
|
+
}
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Collect description lines (paragraphs after story header, before acceptance criteria)
|
|
193
|
+
if (currentStory && !inAcceptanceCriteria && trimmed && !trimmed.startsWith("**") && !trimmed.startsWith("#")) {
|
|
194
|
+
descriptionLines.push(trimmed);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Parse description if in introduction/overview section (for PRD description)
|
|
198
|
+
if ((currentSection === "introduction" || currentSection === "overview") && !currentStory) {
|
|
199
|
+
if (trimmed && !prd.description && !trimmed.startsWith("**")) {
|
|
200
|
+
prd.description = trimmed;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Save last story
|
|
206
|
+
if (currentStory && currentStory.id) {
|
|
207
|
+
if (descriptionLines.length > 0 && !currentStory.description) {
|
|
208
|
+
currentStory.description = descriptionLines.join(" ").trim();
|
|
209
|
+
}
|
|
210
|
+
prd.userStories!.push(currentStory as UserStory);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return prd;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Generate branch name from project name
|
|
218
|
+
*/
|
|
219
|
+
export function generateBranchName(projectName: string, featureName?: string): string {
|
|
220
|
+
let kebab = projectName
|
|
221
|
+
.toLowerCase()
|
|
222
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
223
|
+
.replace(/^-|-$/g, "");
|
|
224
|
+
|
|
225
|
+
// If featureName has a number prefix (NNN-name), prepend it to branch name
|
|
226
|
+
if (featureName) {
|
|
227
|
+
const match = featureName.match(/^(\d{3})-/);
|
|
228
|
+
if (match) {
|
|
229
|
+
const numberPrefix = match[1];
|
|
230
|
+
kebab = `${numberPrefix}-${kebab}`;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return `ralph/${kebab}`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Convert parsed PRD to complete PRD with defaults
|
|
239
|
+
*/
|
|
240
|
+
export function createPRD(parsed: Partial<PRD>, featureName?: string): PRD {
|
|
241
|
+
const project = parsed.project ?? "Unnamed Project";
|
|
242
|
+
const prd: PRD = {
|
|
243
|
+
project,
|
|
244
|
+
branchName: parsed.branchName ?? generateBranchName(project, featureName),
|
|
245
|
+
description: parsed.description ?? "",
|
|
246
|
+
userStories: (parsed.userStories ?? []).map((story, index) => ({
|
|
247
|
+
id: story.id ?? `US-${String(index + 1).padStart(3, "0")}`,
|
|
248
|
+
title: story.title ?? "",
|
|
249
|
+
description: story.description ?? "",
|
|
250
|
+
acceptanceCriteria: story.acceptanceCriteria ?? [],
|
|
251
|
+
priority: story.priority ?? index + 1,
|
|
252
|
+
passes: story.passes ?? false,
|
|
253
|
+
notes: story.notes ?? "",
|
|
254
|
+
dependencies: story.dependencies,
|
|
255
|
+
parallel: story.parallel,
|
|
256
|
+
phase: story.phase,
|
|
257
|
+
research: story.research,
|
|
258
|
+
})),
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// Validate
|
|
262
|
+
return PRDSchema.parse(prd);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Load PRD from JSON file
|
|
267
|
+
*/
|
|
268
|
+
export async function loadPRD(path: string): Promise<PRD> {
|
|
269
|
+
const content = await Bun.file(path).text();
|
|
270
|
+
const json = JSON.parse(content);
|
|
271
|
+
return PRDSchema.parse(json);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Save PRD to JSON file
|
|
276
|
+
*/
|
|
277
|
+
export async function savePRD(prd: PRD, path: string): Promise<void> {
|
|
278
|
+
const validated = PRDSchema.parse(prd);
|
|
279
|
+
const content = JSON.stringify(validated, null, 2);
|
|
280
|
+
await Bun.write(path, content);
|
|
281
|
+
}
|