@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,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