@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,512 @@
1
+ /**
2
+ * Execution Runner
3
+ *
4
+ * Main orchestration loop for running agents with automatic fallback
5
+ */
6
+
7
+ import { existsSync, mkdirSync } from "node:fs";
8
+ import { join, dirname } from "node:path";
9
+ import chalk from "chalk";
10
+ import type { AgentAdapter, AgentName } from "../agents/types";
11
+ import { getAgent, getInstalledAgents } from "../agents/registry";
12
+ import type { RelentlessConfig } from "../config/schema";
13
+ import { loadConstitution, validateConstitution } from "../config/loader";
14
+ import { loadPRD, getNextStory, isComplete, countStories } from "../prd";
15
+ import { loadProgress, updateProgressMetadata, syncPatternsFromContent } from "../prd/progress";
16
+ import { routeStory } from "./router";
17
+
18
+ export interface RunOptions {
19
+ /** Agent to use (or "auto" for smart routing) */
20
+ agent: AgentName | "auto";
21
+ /** Maximum iterations */
22
+ maxIterations: number;
23
+ /** Working directory */
24
+ workingDirectory: string;
25
+ /** Path to prd.json */
26
+ prdPath: string;
27
+ /** Path to prompt.md */
28
+ promptPath: string;
29
+ /** Dry run (don't execute) */
30
+ dryRun: boolean;
31
+ /** Configuration */
32
+ config: RelentlessConfig;
33
+ }
34
+
35
+ export interface RunResult {
36
+ /** Whether all stories completed */
37
+ success: boolean;
38
+ /** Number of iterations executed */
39
+ iterations: number;
40
+ /** Stories completed */
41
+ storiesCompleted: number;
42
+ /** Duration in milliseconds */
43
+ duration: number;
44
+ }
45
+
46
+ /**
47
+ * Track rate limit state for agents
48
+ */
49
+ interface AgentLimitState {
50
+ /** When the limit resets */
51
+ resetTime?: Date;
52
+ /** When we detected the limit */
53
+ detectedAt: Date;
54
+ }
55
+
56
+ /**
57
+ * Build the prompt for an iteration
58
+ */
59
+ async function buildPrompt(
60
+ promptPath: string,
61
+ workingDirectory: string,
62
+ progressPath: string,
63
+ story?: { id: string; research?: boolean }
64
+ ): Promise<string> {
65
+ if (!existsSync(promptPath)) {
66
+ throw new Error(`Prompt file not found: ${promptPath}`);
67
+ }
68
+
69
+ let prompt = await Bun.file(promptPath).text();
70
+
71
+ // Load and append constitution if available
72
+ const constitution = await loadConstitution(workingDirectory);
73
+ if (constitution) {
74
+ // Validate constitution
75
+ const validation = validateConstitution(constitution);
76
+ if (!validation.valid) {
77
+ console.warn(chalk.yellow("\n⚠️ Constitution validation warnings:"));
78
+ for (const error of validation.errors) {
79
+ console.warn(chalk.dim(` - ${error}`));
80
+ }
81
+ }
82
+
83
+ // Append constitution to prompt
84
+ prompt += `\n\n## Project Constitution\n\n`;
85
+ prompt += `The following principles and constraints govern this project.\n\n`;
86
+ prompt += constitution.raw;
87
+ }
88
+
89
+ // Load and append progress patterns if available
90
+ const progress = await loadProgress(progressPath);
91
+ if (progress && progress.metadata.patterns.length > 0) {
92
+ prompt += `\n\n## Learned Patterns from Previous Iterations\n\n`;
93
+ prompt += `The following patterns were discovered in previous iterations:\n\n`;
94
+ for (const pattern of progress.metadata.patterns) {
95
+ prompt += `- ${pattern}\n`;
96
+ }
97
+ }
98
+
99
+ // Load and append plan.md if available
100
+ const planPath = join(dirname(progressPath), "plan.md");
101
+ if (existsSync(planPath)) {
102
+ const planContent = await Bun.file(planPath).text();
103
+ prompt += `\n\n## Technical Planning Document\n\n`;
104
+ prompt += `The following technical plan has been created for this feature:\n\n`;
105
+ prompt += planContent;
106
+ }
107
+
108
+ // Load and append research findings if available
109
+ if (story?.id) {
110
+ const researchPath = join(dirname(progressPath), "research", `${story.id}.md`);
111
+ if (existsSync(researchPath)) {
112
+ const researchContent = await Bun.file(researchPath).text();
113
+ prompt += `\n\n## Research Findings for ${story.id}\n\n`;
114
+ prompt += `The following research was conducted before implementation:\n\n`;
115
+ prompt += researchContent;
116
+ }
117
+ }
118
+
119
+ return prompt;
120
+ }
121
+
122
+ /**
123
+ * Sleep for a given duration
124
+ */
125
+ function sleep(ms: number): Promise<void> {
126
+ return new Promise((resolve) => setTimeout(resolve, ms));
127
+ }
128
+
129
+ /**
130
+ * Check if an agent's rate limit has reset
131
+ */
132
+ function hasLimitReset(state: AgentLimitState): boolean {
133
+ if (state.resetTime) {
134
+ return new Date() >= state.resetTime;
135
+ }
136
+ // If no reset time known, assume 1 hour from detection
137
+ const oneHourAfter = new Date(state.detectedAt.getTime() + 60 * 60 * 1000);
138
+ return new Date() >= oneHourAfter;
139
+ }
140
+
141
+ /**
142
+ * Get the next available agent from the fallback priority list
143
+ */
144
+ async function getNextAvailableAgent(
145
+ priority: AgentName[],
146
+ limitedAgents: Map<AgentName, AgentLimitState>,
147
+ preferredAgent?: AgentName
148
+ ): Promise<AgentAdapter | null> {
149
+ // Get installed agents
150
+ const installed = await getInstalledAgents();
151
+ const installedNames = new Set(installed.map((a) => a.name));
152
+
153
+ // If preferred agent is available and not limited, use it
154
+ if (preferredAgent && installedNames.has(preferredAgent)) {
155
+ const state = limitedAgents.get(preferredAgent);
156
+ if (!state || hasLimitReset(state)) {
157
+ // Clear the limit if it has reset
158
+ if (state && hasLimitReset(state)) {
159
+ limitedAgents.delete(preferredAgent);
160
+ }
161
+ return getAgent(preferredAgent);
162
+ }
163
+ }
164
+
165
+ // Find first available agent in priority order
166
+ for (const name of priority) {
167
+ if (!installedNames.has(name)) {
168
+ continue; // Agent not installed
169
+ }
170
+
171
+ const state = limitedAgents.get(name);
172
+ if (!state) {
173
+ return getAgent(name); // Not limited
174
+ }
175
+
176
+ if (hasLimitReset(state)) {
177
+ limitedAgents.delete(name); // Limit has reset
178
+ return getAgent(name);
179
+ }
180
+ }
181
+
182
+ return null; // All agents are rate limited
183
+ }
184
+
185
+ /**
186
+ * Format time until reset
187
+ */
188
+ function formatTimeUntilReset(resetTime: Date): string {
189
+ const now = new Date();
190
+ const diffMs = resetTime.getTime() - now.getTime();
191
+
192
+ if (diffMs <= 0) {
193
+ return "now";
194
+ }
195
+
196
+ const hours = Math.floor(diffMs / (1000 * 60 * 60));
197
+ const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
198
+
199
+ if (hours > 0) {
200
+ return `${hours}h ${minutes}m`;
201
+ }
202
+ return `${minutes}m`;
203
+ }
204
+
205
+ /**
206
+ * Run the orchestration loop with automatic agent fallback
207
+ */
208
+ export async function run(options: RunOptions): Promise<RunResult> {
209
+ const startTime = Date.now();
210
+ let iterations = 0;
211
+ let storiesCompleted = 0;
212
+
213
+ // Track rate-limited agents
214
+ const limitedAgents = new Map<AgentName, AgentLimitState>();
215
+
216
+ // Current active agent
217
+ let _currentAgentName: AgentName | null = options.agent === "auto" ? null : options.agent;
218
+
219
+ // Load PRD
220
+ if (!existsSync(options.prdPath)) {
221
+ throw new Error(`PRD file not found: ${options.prdPath}`);
222
+ }
223
+ const prd = await loadPRD(options.prdPath);
224
+ const initialCount = countStories(prd);
225
+
226
+ // Calculate progress.txt path
227
+ const prdDir = dirname(options.prdPath);
228
+ const progressPath = join(prdDir, "progress.txt");
229
+
230
+ console.log(chalk.bold.blue("\n🚀 Relentless - Universal AI Agent Orchestrator\n"));
231
+ console.log(`Project: ${chalk.cyan(prd.project)}`);
232
+ console.log(`Branch: ${chalk.cyan(prd.branchName)}`);
233
+ console.log(`Stories: ${chalk.green(initialCount.completed)}/${initialCount.total} complete`);
234
+ console.log(`Max iterations: ${chalk.yellow(options.maxIterations)}`);
235
+
236
+ if (options.config.fallback.enabled) {
237
+ console.log(`Fallback: ${chalk.green("enabled")} (${options.config.fallback.priority.join(" → ")})`);
238
+ }
239
+
240
+ if (options.dryRun) {
241
+ console.log(chalk.yellow("\n⚠️ Dry run mode - not executing\n"));
242
+ }
243
+
244
+ // Main loop
245
+ for (let i = 1; i <= options.maxIterations; i++) {
246
+ iterations = i;
247
+
248
+ // Reload PRD to get latest status
249
+ const currentPRD = await loadPRD(options.prdPath);
250
+
251
+ // Check if complete
252
+ if (isComplete(currentPRD)) {
253
+ console.log(chalk.green.bold("\n✅ All stories complete!"));
254
+ break;
255
+ }
256
+
257
+ // Get next story
258
+ const story = getNextStory(currentPRD);
259
+ if (!story) {
260
+ console.log(chalk.green.bold("\n✅ No more stories to work on!"));
261
+ break;
262
+ }
263
+
264
+ // Select agent
265
+ let agent: AgentAdapter | null = null;
266
+
267
+ if (options.agent === "auto") {
268
+ // Smart routing
269
+ const routedName = routeStory(story, options.config.routing);
270
+ agent = await getNextAvailableAgent(
271
+ options.config.fallback.priority,
272
+ limitedAgents,
273
+ routedName
274
+ );
275
+ if (agent) {
276
+ _currentAgentName = agent.name;
277
+ }
278
+ } else if (options.config.fallback.enabled) {
279
+ // Use fallback if enabled
280
+ agent = await getNextAvailableAgent(
281
+ options.config.fallback.priority,
282
+ limitedAgents,
283
+ options.agent
284
+ );
285
+ if (agent) {
286
+ _currentAgentName = agent.name;
287
+ }
288
+ } else {
289
+ // Use specified agent without fallback
290
+ agent = getAgent(options.agent);
291
+ _currentAgentName = options.agent;
292
+ }
293
+
294
+ // Check if we have an available agent
295
+ if (!agent) {
296
+ console.log(chalk.red.bold("\n❌ All agents are rate limited!"));
297
+
298
+ // Show when limits reset
299
+ for (const [name, state] of limitedAgents) {
300
+ if (state.resetTime) {
301
+ console.log(chalk.dim(` ${name}: resets in ${formatTimeUntilReset(state.resetTime)}`));
302
+ } else {
303
+ console.log(chalk.dim(` ${name}: rate limited (reset time unknown)`));
304
+ }
305
+ }
306
+
307
+ // Find earliest reset time and wait
308
+ const earliestReset = Array.from(limitedAgents.values())
309
+ .filter((s) => s.resetTime)
310
+ .sort((a, b) => (a.resetTime?.getTime() ?? 0) - (b.resetTime?.getTime() ?? 0))[0];
311
+
312
+ if (earliestReset?.resetTime) {
313
+ const waitMs = earliestReset.resetTime.getTime() - Date.now();
314
+ if (waitMs > 0 && waitMs < 60 * 60 * 1000) {
315
+ // Wait up to 1 hour
316
+ console.log(chalk.yellow(`\n⏳ Waiting ${formatTimeUntilReset(earliestReset.resetTime)} for rate limit to reset...`));
317
+ await sleep(waitMs + 1000); // Add 1 second buffer
318
+ continue; // Retry the iteration
319
+ }
320
+ }
321
+
322
+ // Can't continue
323
+ console.log(chalk.yellow("\nStopping - no agents available and reset time too far."));
324
+ break;
325
+ }
326
+
327
+ // Print iteration header
328
+ console.log(chalk.bold(`\n${"═".repeat(60)}`));
329
+ console.log(chalk.bold(` Iteration ${i} of ${options.maxIterations}`));
330
+ console.log(chalk.bold(` Agent: ${chalk.cyan(agent.displayName)}`));
331
+ console.log(chalk.bold(` Story: ${chalk.yellow(story.id)} - ${story.title}`));
332
+ console.log(chalk.bold(`${"═".repeat(60)}\n`));
333
+
334
+ if (options.dryRun) {
335
+ console.log(chalk.dim(" [Dry run - skipping execution]"));
336
+ continue;
337
+ }
338
+
339
+ // Build and run prompt
340
+ try {
341
+ // Check if this story requires research phase
342
+ const researchDir = join(dirname(options.prdPath), "research");
343
+ const researchPath = join(researchDir, `${story.id}.md`);
344
+ const needsResearch = story.research && !existsSync(researchPath);
345
+
346
+ if (needsResearch) {
347
+ // Phase 1: Research
348
+ console.log(chalk.cyan(" 📚 Research phase - gathering context and patterns..."));
349
+
350
+ // Ensure research directory exists
351
+ if (!existsSync(researchDir)) {
352
+ mkdirSync(researchDir, { recursive: true });
353
+ }
354
+
355
+ const researchPrompt = await buildPrompt(options.promptPath, options.workingDirectory, progressPath, story);
356
+ const researchResult = await agent.invoke(researchPrompt, {
357
+ workingDirectory: options.workingDirectory,
358
+ dangerouslyAllowAll: options.config.agents[agent.name]?.dangerouslyAllowAll ?? true,
359
+ model: options.config.agents[agent.name]?.model,
360
+ });
361
+
362
+ // Check for rate limit during research phase
363
+ const researchRateLimit = agent.detectRateLimit(researchResult.output);
364
+ if (researchRateLimit.limited) {
365
+ console.log(chalk.yellow.bold(`\n⚠️ ${agent.displayName} rate limited during research!`));
366
+ limitedAgents.set(agent.name, {
367
+ resetTime: researchRateLimit.resetTime,
368
+ detectedAt: new Date(),
369
+ });
370
+ if (options.config.fallback.enabled) {
371
+ const fallbackAgent = await getNextAvailableAgent(
372
+ options.config.fallback.priority,
373
+ limitedAgents
374
+ );
375
+ if (fallbackAgent) {
376
+ console.log(chalk.green(` Switching to: ${fallbackAgent.displayName}`));
377
+ _currentAgentName = fallbackAgent.name;
378
+ await sleep(options.config.fallback.retryDelay);
379
+ i--;
380
+ continue;
381
+ }
382
+ }
383
+ console.log(chalk.dim(" No fallback agents available."));
384
+ continue;
385
+ }
386
+
387
+ console.log(chalk.green(" ✓ Research phase complete"));
388
+ console.log(chalk.dim(` Research findings saved to: research/${story.id}.md`));
389
+
390
+ // Phase 2: Implementation
391
+ console.log(chalk.cyan(" 🔨 Implementation phase - applying research findings..."));
392
+ }
393
+
394
+ const prompt = await buildPrompt(options.promptPath, options.workingDirectory, progressPath, story);
395
+
396
+ if (!needsResearch) {
397
+ console.log(chalk.dim(" Running agent..."));
398
+ }
399
+
400
+ const result = await agent.invoke(prompt, {
401
+ workingDirectory: options.workingDirectory,
402
+ dangerouslyAllowAll: options.config.agents[agent.name]?.dangerouslyAllowAll ?? true,
403
+ model: options.config.agents[agent.name]?.model,
404
+ });
405
+
406
+ // Check for rate limit
407
+ const rateLimit = agent.detectRateLimit(result.output);
408
+ if (rateLimit.limited) {
409
+ console.log(chalk.yellow.bold(`\n⚠️ ${agent.displayName} rate limited!`));
410
+
411
+ // Track the limit
412
+ limitedAgents.set(agent.name, {
413
+ resetTime: rateLimit.resetTime,
414
+ detectedAt: new Date(),
415
+ });
416
+
417
+ if (rateLimit.resetTime) {
418
+ console.log(chalk.dim(` Resets at: ${rateLimit.resetTime.toLocaleTimeString()}`));
419
+ }
420
+
421
+ // Try to get fallback agent
422
+ if (options.config.fallback.enabled) {
423
+ const fallbackAgent = await getNextAvailableAgent(
424
+ options.config.fallback.priority,
425
+ limitedAgents
426
+ );
427
+
428
+ if (fallbackAgent) {
429
+ console.log(chalk.green(` Switching to: ${fallbackAgent.displayName}`));
430
+ _currentAgentName = fallbackAgent.name;
431
+ // Wait before retry
432
+ await sleep(options.config.fallback.retryDelay);
433
+ i--; // Retry this iteration with new agent
434
+ continue;
435
+ }
436
+ }
437
+
438
+ console.log(chalk.dim(" No fallback agents available."));
439
+ }
440
+
441
+ // Log output (truncated if too long)
442
+ if (result.output) {
443
+ const lines = result.output.split("\n");
444
+ const preview = lines.slice(0, 20).join("\n");
445
+ console.log(chalk.dim(preview));
446
+ if (lines.length > 20) {
447
+ console.log(chalk.dim(` ... (${lines.length - 20} more lines)`));
448
+ }
449
+ }
450
+
451
+ console.log(chalk.dim(`\n Duration: ${(result.duration / 1000).toFixed(1)}s`));
452
+ console.log(chalk.dim(` Exit code: ${result.exitCode}`));
453
+
454
+ // Check for completion signal
455
+ if (result.isComplete) {
456
+ console.log(chalk.green.bold("\n🎉 Agent signaled COMPLETE!"));
457
+
458
+ // Reload and check if really complete
459
+ const finalPRD = await loadPRD(options.prdPath);
460
+ if (isComplete(finalPRD)) {
461
+ storiesCompleted = countStories(finalPRD).completed - initialCount.completed;
462
+ const duration = Date.now() - startTime;
463
+ return { success: true, iterations, storiesCompleted, duration };
464
+ }
465
+ }
466
+
467
+ // Count completed stories
468
+ const updatedPRD = await loadPRD(options.prdPath);
469
+ const updatedCount = countStories(updatedPRD);
470
+ if (updatedCount.completed > initialCount.completed + storiesCompleted) {
471
+ storiesCompleted = updatedCount.completed - initialCount.completed;
472
+ }
473
+
474
+ console.log(chalk.dim(` Stories: ${updatedCount.completed}/${updatedCount.total} complete`));
475
+
476
+ // Update progress metadata after each iteration
477
+ if (existsSync(progressPath)) {
478
+ try {
479
+ // Sync patterns from content
480
+ await syncPatternsFromContent(progressPath);
481
+
482
+ // Update metadata
483
+ await updateProgressMetadata(progressPath, {
484
+ stories_completed: updatedCount.completed,
485
+ });
486
+ } catch (error) {
487
+ console.warn(chalk.yellow(` ⚠️ Failed to update progress metadata: ${error}`));
488
+ }
489
+ }
490
+ } catch (error) {
491
+ console.error(chalk.red(`\n❌ Error in iteration ${i}:`), error);
492
+ // Continue to next iteration
493
+ }
494
+
495
+ // Delay between iterations
496
+ if (i < options.maxIterations) {
497
+ await sleep(options.config.execution.iterationDelay);
498
+ }
499
+ }
500
+
501
+ // Final check
502
+ const finalPRD = await loadPRD(options.prdPath);
503
+ const success = isComplete(finalPRD);
504
+ const duration = Date.now() - startTime;
505
+
506
+ if (!success) {
507
+ console.log(chalk.yellow(`\n⚠️ Reached max iterations (${options.maxIterations}) without completing all stories.`));
508
+ console.log(chalk.dim(`Check progress.txt for status.`));
509
+ }
510
+
511
+ return { success, iterations, storiesCompleted, duration };
512
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Relentless - Universal AI Agent Orchestrator
3
+ *
4
+ * Main entry point for programmatic usage
5
+ */
6
+
7
+ export * from "./agents";
8
+ export * from "./config";
9
+ export * from "./prd";
10
+ export * from "./execution";
11
+ export * from "./init";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Init Module
3
+ *
4
+ * Re-exports initialization functions
5
+ */
6
+
7
+ export * from "./scaffolder";