@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.
Files changed (107) hide show
  1. package/.claude/commands/relentless.analyze.md +20 -0
  2. package/.claude/commands/relentless.checklist.md +15 -0
  3. package/.claude/commands/relentless.clarify.md +19 -0
  4. package/.claude/commands/relentless.constitution.md +78 -0
  5. package/.claude/commands/relentless.implement.md +15 -0
  6. package/.claude/commands/relentless.plan.md +22 -0
  7. package/.claude/commands/relentless.plan.old.md +89 -0
  8. package/.claude/commands/relentless.specify.md +254 -0
  9. package/.claude/commands/relentless.tasks.md +25 -0
  10. package/.claude/commands/relentless.taskstoissues.md +15 -0
  11. package/.claude/settings.local.json +23 -0
  12. package/.claude/skills/analyze/SKILL.md +149 -0
  13. package/.claude/skills/checklist/SKILL.md +173 -0
  14. package/.claude/skills/checklist/templates/checklist-template.md +40 -0
  15. package/.claude/skills/clarify/SKILL.md +174 -0
  16. package/.claude/skills/constitution/SKILL.md +150 -0
  17. package/.claude/skills/constitution/templates/constitution-template.md +228 -0
  18. package/.claude/skills/implement/SKILL.md +141 -0
  19. package/.claude/skills/plan/SKILL.md +179 -0
  20. package/.claude/skills/plan/templates/plan-template.md +104 -0
  21. package/.claude/skills/prd/SKILL.md +242 -0
  22. package/.claude/skills/relentless/SKILL.md +265 -0
  23. package/.claude/skills/specify/SKILL.md +220 -0
  24. package/.claude/skills/specify/scripts/bash/check-prerequisites.sh +166 -0
  25. package/.claude/skills/specify/scripts/bash/common.sh +156 -0
  26. package/.claude/skills/specify/scripts/bash/create-new-feature.sh +305 -0
  27. package/.claude/skills/specify/scripts/bash/setup-plan.sh +61 -0
  28. package/.claude/skills/specify/scripts/bash/update-agent-context.sh +799 -0
  29. package/.claude/skills/specify/templates/spec-template.md +115 -0
  30. package/.claude/skills/tasks/SKILL.md +202 -0
  31. package/.claude/skills/tasks/templates/tasks-template.md +251 -0
  32. package/.claude/skills/taskstoissues/SKILL.md +97 -0
  33. package/.specify/memory/constitution.md +50 -0
  34. package/.specify/scripts/bash/check-prerequisites.sh +166 -0
  35. package/.specify/scripts/bash/common.sh +156 -0
  36. package/.specify/scripts/bash/create-new-feature.sh +297 -0
  37. package/.specify/scripts/bash/setup-plan.sh +61 -0
  38. package/.specify/scripts/bash/update-agent-context.sh +799 -0
  39. package/.specify/templates/agent-file-template.md +28 -0
  40. package/.specify/templates/checklist-template.md +40 -0
  41. package/.specify/templates/plan-template.md +104 -0
  42. package/.specify/templates/spec-template.md +115 -0
  43. package/.specify/templates/tasks-template.md +251 -0
  44. package/CHANGES_SUMMARY.md +255 -0
  45. package/CLAUDE.md +92 -0
  46. package/GEMINI_SETUP.md +256 -0
  47. package/LICENSE +21 -0
  48. package/README.md +1171 -0
  49. package/REFACTOR_SUMMARY.md +267 -0
  50. package/bin/relentless.ts +536 -0
  51. package/bun.lock +352 -0
  52. package/eslint.config.js +37 -0
  53. package/package.json +61 -0
  54. package/prd.json.example +64 -0
  55. package/prompt.md +108 -0
  56. package/ralph.sh +80 -0
  57. package/relentless/config.json +38 -0
  58. package/relentless/features/.gitkeep +0 -0
  59. package/relentless/features/ghsk-ideas/prd.json +229 -0
  60. package/relentless/features/ghsk-ideas/prd.md +191 -0
  61. package/relentless/features/ghsk-ideas/progress.txt +408 -0
  62. package/relentless/prompt.md +79 -0
  63. package/skills/checklist/SKILL.md +349 -0
  64. package/skills/clarify/SKILL.md +476 -0
  65. package/skills/prd/SKILL.md +242 -0
  66. package/skills/relentless/SKILL.md +268 -0
  67. package/skills/tasks/SKILL.md +577 -0
  68. package/src/agents/amp.ts +115 -0
  69. package/src/agents/claude.ts +185 -0
  70. package/src/agents/codex.ts +89 -0
  71. package/src/agents/droid.ts +90 -0
  72. package/src/agents/gemini.ts +109 -0
  73. package/src/agents/index.ts +16 -0
  74. package/src/agents/opencode.ts +88 -0
  75. package/src/agents/registry.ts +95 -0
  76. package/src/agents/types.ts +101 -0
  77. package/src/config/index.ts +8 -0
  78. package/src/config/loader.ts +237 -0
  79. package/src/config/schema.ts +115 -0
  80. package/src/execution/index.ts +8 -0
  81. package/src/execution/router.ts +49 -0
  82. package/src/execution/runner.ts +512 -0
  83. package/src/index.ts +11 -0
  84. package/src/init/index.ts +7 -0
  85. package/src/init/scaffolder.ts +377 -0
  86. package/src/prd/analyzer.ts +512 -0
  87. package/src/prd/index.ts +11 -0
  88. package/src/prd/issues.ts +249 -0
  89. package/src/prd/parser.ts +281 -0
  90. package/src/prd/progress.ts +198 -0
  91. package/src/prd/types.ts +170 -0
  92. package/src/tui/App.tsx +85 -0
  93. package/src/tui/TUIRunner.tsx +400 -0
  94. package/src/tui/components/AgentOutput.tsx +45 -0
  95. package/src/tui/components/AgentStatus.tsx +64 -0
  96. package/src/tui/components/CurrentStory.tsx +66 -0
  97. package/src/tui/components/Header.tsx +49 -0
  98. package/src/tui/components/ProgressBar.tsx +39 -0
  99. package/src/tui/components/StoryGrid.tsx +86 -0
  100. package/src/tui/hooks/useTUI.ts +147 -0
  101. package/src/tui/hooks/useTimer.ts +51 -0
  102. package/src/tui/index.tsx +17 -0
  103. package/src/tui/theme.ts +41 -0
  104. package/src/tui/types.ts +77 -0
  105. package/templates/constitution.md +228 -0
  106. package/templates/plan.md +273 -0
  107. package/tsconfig.json +27 -0
@@ -0,0 +1,536 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Relentless CLI
4
+ *
5
+ * Universal AI agent orchestrator
6
+ */
7
+
8
+ import { Command } from "commander";
9
+ import chalk from "chalk";
10
+ import { join } from "node:path";
11
+ import { existsSync, mkdirSync } from "node:fs";
12
+
13
+ import { checkAgentHealth, getAllAgentNames, isValidAgentName } from "../src/agents";
14
+ import type { AgentName } from "../src/agents/types";
15
+ import { loadConfig, findRelentlessDir } from "../src/config";
16
+ import { loadPRD, savePRD, parsePRDMarkdown, createPRD, analyzeConsistency, formatReport, generateGitHubIssues } from "../src/prd";
17
+ import { run } from "../src/execution/runner";
18
+ import { runTUI } from "../src/tui/TUIRunner";
19
+ import { initProject, createFeature, listFeatures, createProgressTemplate } from "../src/init/scaffolder";
20
+
21
+ const program = new Command();
22
+
23
+ program
24
+ .name("relentless")
25
+ .description("Universal AI agent orchestrator - works with Claude Code, Amp, OpenCode, Codex, Droid, and Gemini")
26
+ .version("0.1.0");
27
+
28
+ // Init command
29
+ program
30
+ .command("init")
31
+ .description("Initialize Relentless in the current project")
32
+ .option("-d, --dir <path>", "Project directory", process.cwd())
33
+ .action(async (options) => {
34
+ await initProject(options.dir);
35
+ });
36
+
37
+ // Run command
38
+ program
39
+ .command("run")
40
+ .description("Run the orchestration loop for a feature")
41
+ .requiredOption("-f, --feature <name>", "Feature name to run")
42
+ .option("-a, --agent <name>", "Agent to use (claude, amp, opencode, codex, droid, gemini, auto)", "claude")
43
+ .option("-m, --max-iterations <n>", "Maximum iterations", "20")
44
+ .option("--dry-run", "Show what would be executed without running", false)
45
+ .option("--tui", "Use beautiful terminal UI interface", false)
46
+ .option("-d, --dir <path>", "Working directory", process.cwd())
47
+ .action(async (options) => {
48
+ const agent = options.agent.toLowerCase();
49
+ if (agent !== "auto" && !isValidAgentName(agent)) {
50
+ console.error(chalk.red(`Invalid agent: ${agent}`));
51
+ console.log(`Valid agents: ${getAllAgentNames().join(", ")}, auto`);
52
+ process.exit(1);
53
+ }
54
+
55
+ const relentlessDir = findRelentlessDir(options.dir);
56
+ if (!relentlessDir) {
57
+ console.error(chalk.red("Relentless not initialized. Run: relentless init"));
58
+ process.exit(1);
59
+ }
60
+
61
+ const featureDir = join(relentlessDir, "features", options.feature);
62
+ if (!existsSync(featureDir)) {
63
+ console.error(chalk.red(`Feature '${options.feature}' not found`));
64
+ console.log(chalk.dim(`Available features: ${listFeatures(options.dir).join(", ") || "none"}`));
65
+ process.exit(1);
66
+ }
67
+
68
+ const prdPath = join(featureDir, "prd.json");
69
+ const promptPath = join(relentlessDir, "prompt.md");
70
+
71
+ if (!existsSync(prdPath)) {
72
+ console.error(chalk.red(`PRD file not found: ${prdPath}`));
73
+ console.log(chalk.dim("Convert a PRD first: relentless convert <prd.md> --feature " + options.feature));
74
+ process.exit(1);
75
+ }
76
+
77
+ if (!existsSync(promptPath)) {
78
+ console.error(chalk.red(`Prompt file not found: ${promptPath}`));
79
+ console.log(chalk.dim("Run 'relentless init' to create default files"));
80
+ process.exit(1);
81
+ }
82
+
83
+ const config = await loadConfig();
84
+
85
+ // Use TUI if requested
86
+ if (options.tui) {
87
+ const success = await runTUI({
88
+ agent: agent as AgentName | "auto",
89
+ maxIterations: parseInt(options.maxIterations, 10),
90
+ workingDirectory: options.dir,
91
+ prdPath,
92
+ promptPath,
93
+ feature: options.feature,
94
+ config,
95
+ dryRun: options.dryRun,
96
+ });
97
+ process.exit(success ? 0 : 1);
98
+ }
99
+
100
+ // Standard runner
101
+ const result = await run({
102
+ agent: agent as AgentName | "auto",
103
+ maxIterations: parseInt(options.maxIterations, 10),
104
+ workingDirectory: options.dir,
105
+ prdPath,
106
+ promptPath,
107
+ dryRun: options.dryRun,
108
+ config,
109
+ });
110
+
111
+ if (result.success) {
112
+ console.log(chalk.green.bold(`\nāœ… Completed successfully!`));
113
+ } else {
114
+ console.log(chalk.yellow(`\nāš ļø Stopped after ${result.iterations} iterations`));
115
+ }
116
+
117
+ console.log(chalk.dim(`\nStats:`));
118
+ console.log(chalk.dim(` Iterations: ${result.iterations}`));
119
+ console.log(chalk.dim(` Stories completed: ${result.storiesCompleted}`));
120
+ console.log(chalk.dim(` Duration: ${(result.duration / 1000 / 60).toFixed(1)} minutes`));
121
+
122
+ process.exit(result.success ? 0 : 1);
123
+ });
124
+
125
+ // Convert command
126
+ program
127
+ .command("convert <tasksMd>")
128
+ .description("Convert tasks.md to prd.json, optionally merging checklist.md criteria")
129
+ .requiredOption("-f, --feature <name>", "Feature name")
130
+ .option("-d, --dir <path>", "Project directory", process.cwd())
131
+ .option("--auto-number", "Auto-number the feature directory (e.g., 001-feature-name)", false)
132
+ .option("--with-checklist", "Merge checklist items into acceptance criteria", false)
133
+ .action(async (tasksMd, options) => {
134
+ if (!existsSync(tasksMd)) {
135
+ console.error(chalk.red(`File not found: ${tasksMd}`));
136
+ process.exit(1);
137
+ }
138
+
139
+ const relentlessDir = findRelentlessDir(options.dir);
140
+ if (!relentlessDir) {
141
+ console.error(chalk.red("Relentless not initialized. Run: relentless init"));
142
+ process.exit(1);
143
+ }
144
+
145
+ // Determine final feature name (with auto-number if requested)
146
+ let finalFeatureName = options.feature;
147
+ if (options.autoNumber) {
148
+ const features = listFeatures(options.dir);
149
+ const numbers = features
150
+ .map((f) => {
151
+ const match = f.match(/^(\d{3})-/);
152
+ return match ? parseInt(match[1], 10) : 0;
153
+ })
154
+ .filter((n) => n > 0);
155
+ const nextNumber = numbers.length > 0 ? Math.max(...numbers) + 1 : 1;
156
+ const numberPrefix = nextNumber.toString().padStart(3, "0");
157
+ finalFeatureName = `${numberPrefix}-${options.feature}`;
158
+ }
159
+
160
+ // Create feature directory if it doesn't exist
161
+ const featureDir = join(relentlessDir, "features", finalFeatureName);
162
+ if (!existsSync(featureDir)) {
163
+ mkdirSync(featureDir, { recursive: true });
164
+ console.log(chalk.dim(`Created feature: ${finalFeatureName}`));
165
+
166
+ // Create progress.txt
167
+ const progressPath = join(featureDir, "progress.txt");
168
+ await Bun.write(progressPath, createProgressTemplate(finalFeatureName));
169
+ }
170
+
171
+ console.log(chalk.dim(`Converting ${tasksMd}...`));
172
+
173
+ // Read tasks.md (primary source)
174
+ const tasksContent = await Bun.file(tasksMd).text();
175
+ const parsed = parsePRDMarkdown(tasksContent);
176
+
177
+ // Optionally merge checklist items
178
+ if (options.withChecklist) {
179
+ const checklistPath = join(featureDir, "checklist.md");
180
+ if (existsSync(checklistPath)) {
181
+ console.log(chalk.dim(" Merging checklist.md criteria..."));
182
+ const checklistContent = await Bun.file(checklistPath).text();
183
+ // TODO: Parse checklist and merge into acceptance criteria
184
+ // For now, just note that it exists
185
+ }
186
+ }
187
+
188
+ const prd = createPRD(parsed, finalFeatureName);
189
+
190
+ // Save prd.json to feature folder
191
+ const prdJsonPath = join(featureDir, "prd.json");
192
+ await savePRD(prd, prdJsonPath);
193
+
194
+ // Also copy the source files to the feature folder
195
+ const tasksMdPath = join(featureDir, "prd.md");
196
+ await Bun.write(tasksMdPath, tasksContent);
197
+
198
+ console.log(chalk.green(`āœ… Created relentless/features/${finalFeatureName}/`));
199
+ console.log(chalk.dim(` prd.json - ${prd.userStories.length} stories`));
200
+ console.log(chalk.dim(` prd.md - from tasks.md`));
201
+ console.log(chalk.dim(` progress.txt - progress log`));
202
+ if (options.withChecklist && existsSync(join(featureDir, "checklist.md"))) {
203
+ console.log(chalk.dim(` āœ“ Checklist criteria merged`));
204
+ }
205
+ console.log(chalk.dim(`\nProject: ${prd.project}`));
206
+ console.log(chalk.dim(`Branch: ${prd.branchName}`));
207
+ });
208
+
209
+ // Feature commands
210
+ const features = program.command("features").description("Manage features");
211
+
212
+ features
213
+ .command("list")
214
+ .description("List all features")
215
+ .option("-d, --dir <path>", "Project directory", process.cwd())
216
+ .action(async (options) => {
217
+ const featureList = listFeatures(options.dir);
218
+
219
+ if (featureList.length === 0) {
220
+ console.log(chalk.dim("\nNo features found."));
221
+ console.log(chalk.dim("Create one with: relentless convert <prd.md> --feature <name>\n"));
222
+ return;
223
+ }
224
+
225
+ console.log(chalk.bold("\nFeatures:\n"));
226
+ for (const feature of featureList) {
227
+ const relentlessDir = findRelentlessDir(options.dir);
228
+ const featureDir = join(relentlessDir!, "features", feature);
229
+ const prdPath = join(featureDir, "prd.json");
230
+
231
+ if (existsSync(prdPath)) {
232
+ try {
233
+ const prdFile = await Bun.file(prdPath).json();
234
+ const completed = prdFile.userStories?.filter((s: { passes?: boolean }) => s.passes).length ?? 0;
235
+ const total = prdFile.userStories?.length ?? 0;
236
+ console.log(` ${feature} - ${completed}/${total} stories complete`);
237
+ } catch {
238
+ console.log(` ${feature}`);
239
+ }
240
+ } else {
241
+ console.log(` ${feature} (no prd.json)`);
242
+ }
243
+ }
244
+ console.log("");
245
+ });
246
+
247
+ features
248
+ .command("create <name>")
249
+ .description("Create a new feature folder")
250
+ .option("-d, --dir <path>", "Project directory", process.cwd())
251
+ .option("--with-plan", "Include plan.md template", false)
252
+ .option("--auto-number", "Auto-number the feature directory (e.g., 001-feature-name)", false)
253
+ .action(async (name, options) => {
254
+ try {
255
+ const featureDir = await createFeature(options.dir, name, {
256
+ withPlan: options.withPlan,
257
+ autoNumber: options.autoNumber,
258
+ });
259
+ const featureName = featureDir.split("/").pop() || name;
260
+ console.log(chalk.green(`āœ… Created feature: ${featureName}`));
261
+ console.log(chalk.dim(` ${featureDir}/`));
262
+ console.log(chalk.dim("\nNext: Add prd.md and run: relentless convert prd.md --feature " + featureName));
263
+ } catch (error) {
264
+ console.error(chalk.red((error as Error).message));
265
+ process.exit(1);
266
+ }
267
+ });
268
+
269
+ // Status command - show all stories with status
270
+ program
271
+ .command("status")
272
+ .description("Show status of all user stories in a feature")
273
+ .requiredOption("-f, --feature <name>", "Feature name")
274
+ .option("-d, --dir <path>", "Project directory", process.cwd())
275
+ .action(async (options) => {
276
+ const relentlessDir = findRelentlessDir(options.dir);
277
+ if (!relentlessDir) {
278
+ console.error(chalk.red("Relentless not initialized. Run: relentless init"));
279
+ process.exit(1);
280
+ }
281
+
282
+ const featureDir = join(relentlessDir, "features", options.feature);
283
+ const prdPath = join(featureDir, "prd.json");
284
+
285
+ if (!existsSync(prdPath)) {
286
+ console.error(chalk.red(`Feature '${options.feature}' not found or has no prd.json`));
287
+ console.log(chalk.dim(`Available features: ${listFeatures(options.dir).join(", ") || "none"}`));
288
+ process.exit(1);
289
+ }
290
+
291
+ const prd = await loadPRD(prdPath);
292
+ const completed = prd.userStories.filter((s) => s.passes).length;
293
+ const total = prd.userStories.length;
294
+
295
+ console.log(chalk.bold(`\nFeature: ${options.feature}`));
296
+ console.log(chalk.dim(`Progress: ${completed}/${total} stories complete\n`));
297
+
298
+ for (const story of prd.userStories) {
299
+ const status = story.passes
300
+ ? chalk.green("āœ“")
301
+ : chalk.dim("ā—‹");
302
+ const id = story.passes
303
+ ? chalk.green(story.id.padEnd(8))
304
+ : chalk.dim(story.id.padEnd(8));
305
+ const title = story.passes
306
+ ? chalk.strikethrough(chalk.dim(story.title))
307
+ : story.title;
308
+ console.log(` ${status} ${id} ${title}`);
309
+ }
310
+ console.log("");
311
+ });
312
+
313
+ // Reset command - reset a story to incomplete
314
+ program
315
+ .command("reset <storyId>")
316
+ .description("Reset a user story to incomplete so it can be re-run")
317
+ .requiredOption("-f, --feature <name>", "Feature name")
318
+ .option("-d, --dir <path>", "Project directory", process.cwd())
319
+ .action(async (storyId, options) => {
320
+ const relentlessDir = findRelentlessDir(options.dir);
321
+ if (!relentlessDir) {
322
+ console.error(chalk.red("Relentless not initialized. Run: relentless init"));
323
+ process.exit(1);
324
+ }
325
+
326
+ const featureDir = join(relentlessDir, "features", options.feature);
327
+ const prdPath = join(featureDir, "prd.json");
328
+
329
+ if (!existsSync(prdPath)) {
330
+ console.error(chalk.red(`Feature '${options.feature}' not found or has no prd.json`));
331
+ process.exit(1);
332
+ }
333
+
334
+ const prd = await loadPRD(prdPath);
335
+ const storyIndex = prd.userStories.findIndex(
336
+ (s) => s.id.toLowerCase() === storyId.toLowerCase()
337
+ );
338
+
339
+ if (storyIndex === -1) {
340
+ console.error(chalk.red(`Story '${storyId}' not found`));
341
+ console.log(chalk.dim(`Available stories: ${prd.userStories.map((s) => s.id).join(", ")}`));
342
+ process.exit(1);
343
+ }
344
+
345
+ const story = prd.userStories[storyIndex];
346
+ const wasComplete = story.passes;
347
+ const prevCompleted = prd.userStories.filter((s) => s.passes).length;
348
+
349
+ // Reset the story
350
+ prd.userStories[storyIndex].passes = false;
351
+ await savePRD(prd, prdPath);
352
+
353
+ const newCompleted = prd.userStories.filter((s) => s.passes).length;
354
+
355
+ if (wasComplete) {
356
+ console.log(chalk.green(`\nāœ“ Reset ${story.id} (${story.title}) to incomplete`));
357
+ console.log(chalk.dim(` Feature: ${options.feature}`));
358
+ console.log(chalk.dim(` Progress: ${newCompleted}/${prd.userStories.length} stories complete (was ${prevCompleted}/${prd.userStories.length})\n`));
359
+ } else {
360
+ console.log(chalk.yellow(`\nā—‹ ${story.id} (${story.title}) was already incomplete\n`));
361
+ }
362
+ });
363
+
364
+ // Agents command
365
+ const agents = program.command("agents").description("Manage AI agents");
366
+
367
+ agents
368
+ .command("list")
369
+ .description("List available agents")
370
+ .action(async () => {
371
+ const health = await checkAgentHealth();
372
+
373
+ console.log(chalk.bold("\nAvailable Agents:\n"));
374
+
375
+ for (const agent of health) {
376
+ const status = agent.installed
377
+ ? chalk.green("āœ“ installed")
378
+ : chalk.red("āœ— not found");
379
+
380
+ console.log(` ${agent.displayName.padEnd(15)} ${status}`);
381
+
382
+ if (agent.installed && agent.executablePath) {
383
+ console.log(chalk.dim(` ${agent.executablePath}`));
384
+ }
385
+
386
+ if (agent.hasSkillSupport && agent.skillInstallCommand) {
387
+ console.log(chalk.dim(` Skills: ${agent.skillInstallCommand}`));
388
+ }
389
+ }
390
+
391
+ console.log("");
392
+ });
393
+
394
+ agents
395
+ .command("doctor")
396
+ .description("Check agent health")
397
+ .action(async () => {
398
+ console.log(chalk.bold("\nšŸ” Agent Health Check\n"));
399
+
400
+ const health = await checkAgentHealth();
401
+ let allHealthy = true;
402
+
403
+ for (const agent of health) {
404
+ if (agent.installed) {
405
+ console.log(`${chalk.green("āœ“")} ${agent.displayName}`);
406
+ console.log(chalk.dim(` Path: ${agent.executablePath}`));
407
+ if (agent.hasSkillSupport) {
408
+ console.log(chalk.dim(` Skills: Supported`));
409
+ }
410
+ } else {
411
+ console.log(`${chalk.red("āœ—")} ${agent.displayName}`);
412
+ allHealthy = false;
413
+ }
414
+ }
415
+
416
+ console.log("");
417
+
418
+ if (allHealthy) {
419
+ console.log(chalk.green("All agents healthy!"));
420
+ } else {
421
+ console.log(chalk.yellow("Some agents are not installed."));
422
+ }
423
+ });
424
+
425
+ // Analyze command
426
+ program
427
+ .command("analyze")
428
+ .description("Analyze cross-artifact consistency for a feature")
429
+ .requiredOption("-f, --feature <name>", "Feature name")
430
+ .option("-d, --dir <path>", "Project directory", process.cwd())
431
+ .action(async (options) => {
432
+ const relentlessDir = findRelentlessDir(options.dir);
433
+ if (!relentlessDir) {
434
+ console.error(chalk.red("Relentless not initialized. Run: relentless init"));
435
+ process.exit(1);
436
+ }
437
+
438
+ const featureDir = join(relentlessDir, "features", options.feature);
439
+ const prdPath = join(featureDir, "prd.json");
440
+
441
+ if (!existsSync(prdPath)) {
442
+ console.error(chalk.red(`Feature '${options.feature}' not found or has no prd.json`));
443
+ console.log(chalk.dim(`Available features: ${listFeatures(options.dir).join(", ") || "none"}`));
444
+ process.exit(1);
445
+ }
446
+
447
+ const prd = await loadPRD(prdPath);
448
+ const report = await analyzeConsistency(prdPath, prd);
449
+
450
+ const formatted = formatReport(report);
451
+ console.log(formatted);
452
+
453
+ // Exit with error code if there are critical issues
454
+ if (report.summary.critical > 0) {
455
+ console.log(chalk.red("\nāŒ Critical issues found. Please address them before proceeding.\n"));
456
+ process.exit(1);
457
+ } else if (report.summary.warnings > 0) {
458
+ console.log(chalk.yellow("\nāš ļø Warnings found. Consider addressing them.\n"));
459
+ } else {
460
+ console.log(chalk.green("\nāœ… No critical issues or warnings found!\n"));
461
+ }
462
+ });
463
+
464
+ // Issues command
465
+ program
466
+ .command("issues")
467
+ .description("Convert user stories to GitHub issues")
468
+ .requiredOption("-f, --feature <name>", "Feature name")
469
+ .option("-d, --dir <path>", "Project directory", process.cwd())
470
+ .option("--all", "Include completed stories (default: only incomplete)", false)
471
+ .option("--dry-run", "Show what would be created without actually creating issues", false)
472
+ .action(async (options) => {
473
+ const relentlessDir = findRelentlessDir(options.dir);
474
+ if (!relentlessDir) {
475
+ console.error(chalk.red("Relentless not initialized. Run: relentless init"));
476
+ process.exit(1);
477
+ }
478
+
479
+ const featureDir = join(relentlessDir, "features", options.feature);
480
+ const prdPath = join(featureDir, "prd.json");
481
+
482
+ if (!existsSync(prdPath)) {
483
+ console.error(chalk.red(`Feature '${options.feature}' not found or has no prd.json`));
484
+ console.log(chalk.dim(`Available features: ${listFeatures(options.dir).join(", ") || "none"}`));
485
+ process.exit(1);
486
+ }
487
+
488
+ const prd = await loadPRD(prdPath);
489
+
490
+ console.log(chalk.bold(`\nšŸ“‹ GitHub Issues Generator\n`));
491
+ console.log(chalk.dim(`Feature: ${options.feature}`));
492
+ console.log(chalk.dim(`Project: ${prd.project}\n`));
493
+
494
+ try {
495
+ const result = await generateGitHubIssues(prd, {
496
+ dryRun: options.dryRun,
497
+ onlyIncomplete: !options.all,
498
+ });
499
+
500
+ console.log("");
501
+ if (options.dryRun) {
502
+ console.log(chalk.yellow(`Would create ${result.created} issues`));
503
+ } else {
504
+ console.log(chalk.green(`āœ… Created ${result.created} issues`));
505
+ }
506
+
507
+ if (result.errors.length > 0) {
508
+ console.log(chalk.red(`\nāŒ ${result.errors.length} errors occurred:`));
509
+ for (const error of result.errors) {
510
+ console.log(chalk.dim(` ${error}`));
511
+ }
512
+ process.exit(1);
513
+ }
514
+ } catch (error) {
515
+ console.error(chalk.red(`\nāŒ ${(error as Error).message}\n`));
516
+ process.exit(1);
517
+ }
518
+ });
519
+
520
+ // PRD command (for agents without skill support)
521
+ program
522
+ .command("prd <description>")
523
+ .description("Generate a PRD using prompting (for agents without skill support)")
524
+ .option("-a, --agent <name>", "Agent to use", "claude")
525
+ .option("-f, --feature <name>", "Feature name")
526
+ .action(async (description, _options) => {
527
+ console.log(chalk.bold("\nšŸ“ PRD Generation\n"));
528
+ console.log(chalk.dim("For agents with skill support, use:"));
529
+ console.log(chalk.cyan(' claude "Load the prd skill and create a PRD for ' + description + '"\n'));
530
+
531
+ console.log(chalk.yellow("Direct PRD generation coming soon..."));
532
+ // TODO: Implement direct PRD generation for agents without skill support
533
+ });
534
+
535
+ // Parse arguments
536
+ program.parse();