@arvorco/relentless 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/relentless.constitution.md +1 -1
- package/.claude/commands/relentless.specify.md +1 -1
- package/.claude/skills/specify/scripts/bash/create-new-feature.sh +2 -2
- package/.claude/skills/specify/scripts/bash/setup-plan.sh +1 -1
- package/CHANGELOG.md +95 -1
- package/README.md +205 -983
- package/Relentless.png +0 -0
- package/Relentless.svg +1 -0
- package/bin/relentless.ts +121 -0
- package/package.json +1 -1
- package/src/cli/queue.ts +406 -0
- package/src/execution/commands.ts +541 -0
- package/src/execution/runner.ts +236 -4
- package/src/prd/parser.ts +140 -0
- package/src/prd/types.ts +8 -6
- package/src/queue/index.ts +45 -0
- package/src/queue/loader.ts +97 -0
- package/src/queue/lock.ts +137 -0
- package/src/queue/parser.ts +142 -0
- package/src/queue/processor.ts +141 -0
- package/src/queue/types.ts +81 -0
- package/src/queue/writer.ts +210 -0
- package/src/tui/App.tsx +23 -0
- package/src/tui/TUIRunner.tsx +183 -2
- package/src/tui/components/QueueInput.tsx +160 -0
- package/src/tui/components/QueuePanel.tsx +169 -0
- package/src/tui/components/QueueRemoval.tsx +306 -0
- package/src/tui/hooks/useTUI.ts +6 -0
- package/src/tui/types.ts +13 -0
- package/.claude/skills/specify/scripts/bash/update-agent-context.sh +0 -799
package/src/execution/runner.ts
CHANGED
|
@@ -11,11 +11,29 @@ import type { AgentAdapter, AgentName } from "../agents/types";
|
|
|
11
11
|
import { getAgent, getInstalledAgents } from "../agents/registry";
|
|
12
12
|
import type { RelentlessConfig } from "../config/schema";
|
|
13
13
|
import { loadConstitution, validateConstitution } from "../config/loader";
|
|
14
|
-
import { loadPRD, getNextStory, isComplete, countStories } from "../prd";
|
|
14
|
+
import { loadPRD, getNextStory, isComplete, countStories, markStoryAsSkipped } from "../prd";
|
|
15
15
|
import type { UserStory } from "../prd/types";
|
|
16
|
-
import { loadProgress, updateProgressMetadata, syncPatternsFromContent } from "../prd/progress";
|
|
16
|
+
import { loadProgress, updateProgressMetadata, syncPatternsFromContent, appendProgress } from "../prd/progress";
|
|
17
17
|
import { routeStory } from "./router";
|
|
18
18
|
import { buildStoryPromptAddition } from "./story-prompt";
|
|
19
|
+
import { processQueue } from "../queue";
|
|
20
|
+
import type { QueueProcessResult } from "../queue/types";
|
|
21
|
+
import {
|
|
22
|
+
shouldPause,
|
|
23
|
+
executePauseAction,
|
|
24
|
+
logPauseToProgress,
|
|
25
|
+
formatPauseMessage,
|
|
26
|
+
shouldAbort,
|
|
27
|
+
logAbortToProgress,
|
|
28
|
+
formatAbortMessage,
|
|
29
|
+
generateAbortSummary,
|
|
30
|
+
shouldSkip,
|
|
31
|
+
getSkipCommands,
|
|
32
|
+
handleSkipCommand,
|
|
33
|
+
logSkipToProgress,
|
|
34
|
+
logSkipRejectedToProgress,
|
|
35
|
+
formatSkipMessage,
|
|
36
|
+
} from "./commands";
|
|
19
37
|
|
|
20
38
|
export interface RunOptions {
|
|
21
39
|
/** Agent to use (or "auto" for smart routing) */
|
|
@@ -55,6 +73,106 @@ interface AgentLimitState {
|
|
|
55
73
|
detectedAt: Date;
|
|
56
74
|
}
|
|
57
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Process queue items for an iteration
|
|
78
|
+
*
|
|
79
|
+
* Reads pending queue items and processes them.
|
|
80
|
+
* This is called at the start of each iteration.
|
|
81
|
+
*
|
|
82
|
+
* @param featurePath - Path to the feature directory
|
|
83
|
+
* @returns QueueProcessResult with prompts, commands, and warnings
|
|
84
|
+
*/
|
|
85
|
+
export async function processQueueForIteration(
|
|
86
|
+
featurePath: string
|
|
87
|
+
): Promise<QueueProcessResult> {
|
|
88
|
+
return processQueue(featurePath);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Inject queue prompts into the agent prompt
|
|
93
|
+
*
|
|
94
|
+
* Adds a "Queued User Guidance" section to the prompt with
|
|
95
|
+
* numbered list of user messages from the queue.
|
|
96
|
+
*
|
|
97
|
+
* @param basePrompt - The original prompt
|
|
98
|
+
* @param prompts - Queue prompts to inject
|
|
99
|
+
* @returns Modified prompt with queue guidance section
|
|
100
|
+
*/
|
|
101
|
+
export function injectQueuePrompts(basePrompt: string, prompts: string[]): string {
|
|
102
|
+
if (prompts.length === 0) {
|
|
103
|
+
return basePrompt;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const numberedList = prompts.map((p, i) => `${i + 1}. ${p}`).join("\n");
|
|
107
|
+
|
|
108
|
+
const queueSection = `
|
|
109
|
+
|
|
110
|
+
## Queued User Guidance
|
|
111
|
+
|
|
112
|
+
The following messages were queued by the user during the run. Please incorporate this guidance into your work:
|
|
113
|
+
|
|
114
|
+
${numberedList}
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
`;
|
|
118
|
+
|
|
119
|
+
return basePrompt + queueSection;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Acknowledge queue processing in progress.txt
|
|
124
|
+
*
|
|
125
|
+
* Appends a note about processed queue items to the progress log.
|
|
126
|
+
*
|
|
127
|
+
* @param progressPath - Path to progress.txt
|
|
128
|
+
* @param prompts - Prompts that were processed
|
|
129
|
+
*/
|
|
130
|
+
export async function acknowledgeQueueInProgress(
|
|
131
|
+
progressPath: string,
|
|
132
|
+
prompts: string[]
|
|
133
|
+
): Promise<void> {
|
|
134
|
+
if (prompts.length === 0) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const timestamp = new Date().toISOString().split("T")[0];
|
|
139
|
+
const entry = `
|
|
140
|
+
## Queue Processed - ${timestamp}
|
|
141
|
+
|
|
142
|
+
Acknowledged ${prompts.length} queued message(s):
|
|
143
|
+
${prompts.map((p) => `- ${p}`).join("\n")}
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
`;
|
|
147
|
+
|
|
148
|
+
await appendProgress(progressPath, entry);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Format queue log message for console output
|
|
153
|
+
*
|
|
154
|
+
* @param promptCount - Number of prompts in queue
|
|
155
|
+
* @param commandCount - Number of commands in queue
|
|
156
|
+
* @returns Formatted log message
|
|
157
|
+
*/
|
|
158
|
+
export function formatQueueLogMessage(promptCount: number, commandCount: number): string {
|
|
159
|
+
const total = promptCount + commandCount;
|
|
160
|
+
|
|
161
|
+
if (total === 0) {
|
|
162
|
+
return "";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const itemWord = total === 1 ? "item" : "items";
|
|
166
|
+
let message = `Processing ${total} queue ${itemWord}...`;
|
|
167
|
+
|
|
168
|
+
if (commandCount > 0) {
|
|
169
|
+
const cmdWord = commandCount === 1 ? "command" : "commands";
|
|
170
|
+
message = `Processing ${total} queue ${itemWord} (${commandCount} ${cmdWord})...`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return message;
|
|
174
|
+
}
|
|
175
|
+
|
|
58
176
|
/**
|
|
59
177
|
* Build the prompt for an iteration
|
|
60
178
|
*/
|
|
@@ -370,6 +488,109 @@ export async function run(options: RunOptions): Promise<RunResult> {
|
|
|
370
488
|
console.log(chalk.bold(` Story: ${chalk.yellow(story.id)} - ${story.title}`));
|
|
371
489
|
console.log(chalk.bold(`${"═".repeat(60)}\n`));
|
|
372
490
|
|
|
491
|
+
// Process queue at start of iteration
|
|
492
|
+
const featureDir = dirname(options.prdPath);
|
|
493
|
+
let queuePrompts: string[] = [];
|
|
494
|
+
try {
|
|
495
|
+
const queueResult = await processQueueForIteration(featureDir);
|
|
496
|
+
queuePrompts = queueResult.prompts;
|
|
497
|
+
|
|
498
|
+
// Log if there are queue items (silent for empty queue)
|
|
499
|
+
const logMessage = formatQueueLogMessage(queueResult.prompts.length, queueResult.commands.length);
|
|
500
|
+
if (logMessage) {
|
|
501
|
+
console.log(chalk.cyan(` 📬 ${logMessage}`));
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Log warnings if any
|
|
505
|
+
for (const warning of queueResult.warnings) {
|
|
506
|
+
console.log(chalk.yellow(` ⚠️ ${warning}`));
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Acknowledge in progress.txt
|
|
510
|
+
if (queueResult.prompts.length > 0 && existsSync(progressPath)) {
|
|
511
|
+
await acknowledgeQueueInProgress(progressPath, queueResult.prompts);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Handle PAUSE command
|
|
515
|
+
if (shouldPause(queueResult.commands)) {
|
|
516
|
+
console.log(chalk.yellow(`\n ${formatPauseMessage()}`));
|
|
517
|
+
|
|
518
|
+
// Log pause to progress.txt
|
|
519
|
+
if (existsSync(progressPath)) {
|
|
520
|
+
await logPauseToProgress(progressPath);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Wait for user input
|
|
524
|
+
await executePauseAction();
|
|
525
|
+
console.log(chalk.green(" ▶️ Resuming...\n"));
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Handle ABORT command
|
|
529
|
+
if (shouldAbort(queueResult.commands)) {
|
|
530
|
+
console.log(chalk.red(`\n ${formatAbortMessage()}`));
|
|
531
|
+
|
|
532
|
+
// Log abort to progress.txt
|
|
533
|
+
if (existsSync(progressPath)) {
|
|
534
|
+
await logAbortToProgress(progressPath);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Calculate summary
|
|
538
|
+
const currentPRDForAbort = await loadPRD(options.prdPath);
|
|
539
|
+
const currentCount = countStories(currentPRDForAbort);
|
|
540
|
+
const duration = Date.now() - startTime;
|
|
541
|
+
|
|
542
|
+
// Show progress summary
|
|
543
|
+
const summary = generateAbortSummary({
|
|
544
|
+
storiesCompleted: currentCount.completed,
|
|
545
|
+
storiesTotal: currentCount.total,
|
|
546
|
+
iterations: i,
|
|
547
|
+
duration,
|
|
548
|
+
});
|
|
549
|
+
console.log(chalk.dim(summary));
|
|
550
|
+
|
|
551
|
+
// Return with success (clean exit)
|
|
552
|
+
return {
|
|
553
|
+
success: false, // Not all stories complete
|
|
554
|
+
iterations: i,
|
|
555
|
+
storiesCompleted: currentCount.completed - initialCount.completed,
|
|
556
|
+
duration,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Handle SKIP command(s)
|
|
561
|
+
if (shouldSkip(queueResult.commands)) {
|
|
562
|
+
const skipCommands = getSkipCommands(queueResult.commands);
|
|
563
|
+
for (const skipCmd of skipCommands) {
|
|
564
|
+
// Check if trying to skip the current story in progress
|
|
565
|
+
const action = handleSkipCommand(skipCmd.storyId, story.id);
|
|
566
|
+
|
|
567
|
+
if (action.rejected) {
|
|
568
|
+
// Log rejection to console and progress.txt
|
|
569
|
+
console.log(chalk.yellow(`\n ${formatSkipMessage(action.storyId, true)}`));
|
|
570
|
+
if (existsSync(progressPath)) {
|
|
571
|
+
await logSkipRejectedToProgress(progressPath, action.storyId);
|
|
572
|
+
}
|
|
573
|
+
} else {
|
|
574
|
+
// Mark story as skipped in PRD
|
|
575
|
+
const skipResult = await markStoryAsSkipped(options.prdPath, action.storyId);
|
|
576
|
+
|
|
577
|
+
if (skipResult.success) {
|
|
578
|
+
console.log(chalk.cyan(`\n ${formatSkipMessage(action.storyId, false)}`));
|
|
579
|
+
if (existsSync(progressPath)) {
|
|
580
|
+
await logSkipToProgress(progressPath, action.storyId, action.customReason);
|
|
581
|
+
}
|
|
582
|
+
} else if (skipResult.error) {
|
|
583
|
+
console.log(chalk.yellow(`\n ⚠️ ${skipResult.error}`));
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// TODO: Handle PRIORITY command in future story (US-012)
|
|
590
|
+
} catch (queueError) {
|
|
591
|
+
console.warn(chalk.yellow(` ⚠️ Queue processing error: ${queueError}`));
|
|
592
|
+
}
|
|
593
|
+
|
|
373
594
|
if (options.dryRun) {
|
|
374
595
|
console.log(chalk.dim(" [Dry run - skipping execution]"));
|
|
375
596
|
continue;
|
|
@@ -391,7 +612,13 @@ export async function run(options: RunOptions): Promise<RunResult> {
|
|
|
391
612
|
mkdirSync(researchDir, { recursive: true });
|
|
392
613
|
}
|
|
393
614
|
|
|
394
|
-
|
|
615
|
+
let researchPrompt = await buildPrompt(options.promptPath, options.workingDirectory, progressPath, story);
|
|
616
|
+
|
|
617
|
+
// Inject queue prompts into research phase too
|
|
618
|
+
if (queuePrompts.length > 0) {
|
|
619
|
+
researchPrompt = injectQueuePrompts(researchPrompt, queuePrompts);
|
|
620
|
+
}
|
|
621
|
+
|
|
395
622
|
const researchResult = await agent.invoke(researchPrompt, {
|
|
396
623
|
workingDirectory: options.workingDirectory,
|
|
397
624
|
dangerouslyAllowAll: options.config.agents[agent.name]?.dangerouslyAllowAll ?? true,
|
|
@@ -430,7 +657,12 @@ export async function run(options: RunOptions): Promise<RunResult> {
|
|
|
430
657
|
console.log(chalk.cyan(" 🔨 Implementation phase - applying research findings..."));
|
|
431
658
|
}
|
|
432
659
|
|
|
433
|
-
|
|
660
|
+
let prompt = await buildPrompt(options.promptPath, options.workingDirectory, progressPath, story);
|
|
661
|
+
|
|
662
|
+
// Inject queue prompts if any
|
|
663
|
+
if (queuePrompts.length > 0) {
|
|
664
|
+
prompt = injectQueuePrompts(prompt, queuePrompts);
|
|
665
|
+
}
|
|
434
666
|
|
|
435
667
|
if (!needsResearch) {
|
|
436
668
|
console.log(chalk.dim(" Running agent..."));
|
package/src/prd/parser.ts
CHANGED
|
@@ -279,3 +279,143 @@ export async function savePRD(prd: PRD, path: string): Promise<void> {
|
|
|
279
279
|
const content = JSON.stringify(validated, null, 2);
|
|
280
280
|
await Bun.write(path, content);
|
|
281
281
|
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Result of marking a story as skipped
|
|
285
|
+
*/
|
|
286
|
+
export interface MarkSkippedResult {
|
|
287
|
+
success: boolean;
|
|
288
|
+
error?: string;
|
|
289
|
+
alreadySkipped?: boolean;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Result of prioritizing a story
|
|
294
|
+
*/
|
|
295
|
+
export interface PrioritizeStoryResult {
|
|
296
|
+
success: boolean;
|
|
297
|
+
error?: string;
|
|
298
|
+
previousPriority?: number;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Mark a story as skipped in the PRD file
|
|
303
|
+
*
|
|
304
|
+
* @param prdPath - Path to the prd.json file
|
|
305
|
+
* @param storyId - The ID of the story to skip
|
|
306
|
+
* @returns Result indicating success or failure
|
|
307
|
+
*/
|
|
308
|
+
export async function markStoryAsSkipped(
|
|
309
|
+
prdPath: string,
|
|
310
|
+
storyId: string
|
|
311
|
+
): Promise<MarkSkippedResult> {
|
|
312
|
+
// Load current PRD
|
|
313
|
+
const prd = await loadPRD(prdPath);
|
|
314
|
+
|
|
315
|
+
// Find the story
|
|
316
|
+
const storyIndex = prd.userStories.findIndex((s) => s.id === storyId);
|
|
317
|
+
if (storyIndex === -1) {
|
|
318
|
+
return {
|
|
319
|
+
success: false,
|
|
320
|
+
error: `Story ${storyId} not found in PRD`,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const story = prd.userStories[storyIndex];
|
|
325
|
+
|
|
326
|
+
// Check if story is already completed
|
|
327
|
+
if (story.passes) {
|
|
328
|
+
return {
|
|
329
|
+
success: false,
|
|
330
|
+
error: `Story ${storyId} is already completed and cannot be skipped`,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Check if already skipped
|
|
335
|
+
if (story.skipped) {
|
|
336
|
+
return {
|
|
337
|
+
success: true,
|
|
338
|
+
alreadySkipped: true,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Mark as skipped
|
|
343
|
+
prd.userStories[storyIndex] = {
|
|
344
|
+
...story,
|
|
345
|
+
skipped: true,
|
|
346
|
+
notes: story.notes
|
|
347
|
+
? `${story.notes}\n[SKIPPED] ${new Date().toISOString().split("T")[0]}`
|
|
348
|
+
: `[SKIPPED] ${new Date().toISOString().split("T")[0]}`,
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
// Save the updated PRD
|
|
352
|
+
await savePRD(prd, prdPath);
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
success: true,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Prioritize a story to be the next one worked on
|
|
361
|
+
*
|
|
362
|
+
* Sets the story's priority to 0 (highest priority) so it becomes
|
|
363
|
+
* the next story selected by getNextStory().
|
|
364
|
+
*
|
|
365
|
+
* @param prdPath - Path to the prd.json file
|
|
366
|
+
* @param storyId - The ID of the story to prioritize
|
|
367
|
+
* @returns Result indicating success or failure
|
|
368
|
+
*/
|
|
369
|
+
export async function prioritizeStory(
|
|
370
|
+
prdPath: string,
|
|
371
|
+
storyId: string
|
|
372
|
+
): Promise<PrioritizeStoryResult> {
|
|
373
|
+
// Load current PRD
|
|
374
|
+
const prd = await loadPRD(prdPath);
|
|
375
|
+
|
|
376
|
+
// Find the story
|
|
377
|
+
const storyIndex = prd.userStories.findIndex((s) => s.id === storyId);
|
|
378
|
+
if (storyIndex === -1) {
|
|
379
|
+
return {
|
|
380
|
+
success: false,
|
|
381
|
+
error: `Story ${storyId} not found in PRD`,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const story = prd.userStories[storyIndex];
|
|
386
|
+
|
|
387
|
+
// Check if story is already completed
|
|
388
|
+
if (story.passes) {
|
|
389
|
+
return {
|
|
390
|
+
success: false,
|
|
391
|
+
error: `Story ${storyId} is already completed and cannot be prioritized`,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Check if story is skipped
|
|
396
|
+
if (story.skipped) {
|
|
397
|
+
return {
|
|
398
|
+
success: false,
|
|
399
|
+
error: `Story ${storyId} is skipped and cannot be prioritized`,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const previousPriority = story.priority;
|
|
404
|
+
|
|
405
|
+
// Set priority to 0 (highest priority)
|
|
406
|
+
prd.userStories[storyIndex] = {
|
|
407
|
+
...story,
|
|
408
|
+
priority: 0,
|
|
409
|
+
notes: story.notes
|
|
410
|
+
? `${story.notes}\n[PRIORITIZED] ${new Date().toISOString().split("T")[0]} (was priority ${previousPriority})`
|
|
411
|
+
: `[PRIORITIZED] ${new Date().toISOString().split("T")[0]} (was priority ${previousPriority})`,
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// Save the updated PRD
|
|
415
|
+
await savePRD(prd, prdPath);
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
success: true,
|
|
419
|
+
previousPriority,
|
|
420
|
+
};
|
|
421
|
+
}
|
package/src/prd/types.ts
CHANGED
|
@@ -14,13 +14,14 @@ export const UserStorySchema = z.object({
|
|
|
14
14
|
title: z.string(),
|
|
15
15
|
description: z.string(),
|
|
16
16
|
acceptanceCriteria: z.array(z.string()),
|
|
17
|
-
priority: z.number().int().
|
|
17
|
+
priority: z.number().int().nonnegative(), // 0 = highest priority (used by PRIORITY command)
|
|
18
18
|
passes: z.boolean().default(false),
|
|
19
19
|
notes: z.string().default(""),
|
|
20
20
|
dependencies: z.array(z.string()).optional(), // Array of story IDs this story depends on
|
|
21
21
|
parallel: z.boolean().optional(), // Can be executed in parallel with other stories
|
|
22
22
|
phase: z.string().optional(), // Phase marker (e.g., "Setup", "Foundation", "Stories", "Polish")
|
|
23
23
|
research: z.boolean().optional(), // Requires research phase before implementation
|
|
24
|
+
skipped: z.boolean().optional(), // Whether the story was skipped by user command
|
|
24
25
|
});
|
|
25
26
|
|
|
26
27
|
export type UserStory = z.infer<typeof UserStorySchema>;
|
|
@@ -116,9 +117,9 @@ export function getNextStory(prd: PRD): UserStory | null {
|
|
|
116
117
|
// Validate dependencies first
|
|
117
118
|
validateDependencies(prd);
|
|
118
119
|
|
|
119
|
-
// Find highest priority story where passes is false and dependencies are met
|
|
120
|
+
// Find highest priority story where passes is false, not skipped, and dependencies are met
|
|
120
121
|
const pendingStories = prd.userStories
|
|
121
|
-
.filter((s) => !s.passes && areDependenciesMet(s, prd))
|
|
122
|
+
.filter((s) => !s.passes && !s.skipped && areDependenciesMet(s, prd))
|
|
122
123
|
.sort((a, b) => a.priority - b.priority);
|
|
123
124
|
|
|
124
125
|
return pendingStories[0] ?? null;
|
|
@@ -134,11 +135,12 @@ export function isComplete(prd: PRD): boolean {
|
|
|
134
135
|
/**
|
|
135
136
|
* Count stories by status
|
|
136
137
|
*/
|
|
137
|
-
export function countStories(prd: PRD): { total: number; completed: number; pending: number } {
|
|
138
|
+
export function countStories(prd: PRD): { total: number; completed: number; skipped: number; pending: number } {
|
|
138
139
|
const total = prd.userStories.length;
|
|
139
140
|
const completed = prd.userStories.filter((s) => s.passes).length;
|
|
140
|
-
const
|
|
141
|
-
|
|
141
|
+
const skipped = prd.userStories.filter((s) => s.skipped && !s.passes).length;
|
|
142
|
+
const pending = total - completed - skipped;
|
|
143
|
+
return { total, completed, skipped, pending };
|
|
142
144
|
}
|
|
143
145
|
|
|
144
146
|
/**
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue Module
|
|
3
|
+
*
|
|
4
|
+
* Provides queue management for mid-run user input.
|
|
5
|
+
* Agents check the queue between iterations to receive
|
|
6
|
+
* user guidance and structured commands.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Export types
|
|
10
|
+
export type {
|
|
11
|
+
QueueItem,
|
|
12
|
+
QueueState,
|
|
13
|
+
QueueCommandType,
|
|
14
|
+
QueueItemType,
|
|
15
|
+
QueueProcessResult,
|
|
16
|
+
ParsedCommand,
|
|
17
|
+
} from "./types";
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
QueueItemSchema,
|
|
21
|
+
QueueStateSchema,
|
|
22
|
+
QueueCommandTypeSchema,
|
|
23
|
+
QueueItemTypeSchema,
|
|
24
|
+
} from "./types";
|
|
25
|
+
|
|
26
|
+
// Export parser functions
|
|
27
|
+
export { parseQueueLine, parseCommand, formatQueueLine } from "./parser";
|
|
28
|
+
|
|
29
|
+
// Export writer functions
|
|
30
|
+
export { addToQueue, removeFromQueue, clearQueue } from "./writer";
|
|
31
|
+
|
|
32
|
+
// Export loader functions
|
|
33
|
+
export { loadQueue } from "./loader";
|
|
34
|
+
|
|
35
|
+
// Export processor functions
|
|
36
|
+
export { processQueue } from "./processor";
|
|
37
|
+
|
|
38
|
+
// Export lock functions
|
|
39
|
+
export {
|
|
40
|
+
acquireQueueLock,
|
|
41
|
+
releaseQueueLock,
|
|
42
|
+
isQueueLocked,
|
|
43
|
+
setLockTimeout,
|
|
44
|
+
getLockTimeout,
|
|
45
|
+
} from "./lock";
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue Loader
|
|
3
|
+
*
|
|
4
|
+
* Functions for loading queue state from files.
|
|
5
|
+
* Handles pending and processed items with graceful error handling.
|
|
6
|
+
*
|
|
7
|
+
* Queue file locations:
|
|
8
|
+
* - Pending: <featurePath>/.queue.txt
|
|
9
|
+
* - Processed: <featurePath>/.queue.processed.txt
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import type { QueueItem, QueueState } from "./types";
|
|
14
|
+
import { parseQueueLine } from "./parser";
|
|
15
|
+
|
|
16
|
+
/** Pending queue file name */
|
|
17
|
+
const QUEUE_FILE = ".queue.txt";
|
|
18
|
+
|
|
19
|
+
/** Processed queue file name */
|
|
20
|
+
const QUEUE_PROCESSED_FILE = ".queue.processed.txt";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Load queue state from files
|
|
24
|
+
*
|
|
25
|
+
* @param featurePath - Path to the feature directory
|
|
26
|
+
* @returns QueueState with pending and processed items
|
|
27
|
+
*/
|
|
28
|
+
export async function loadQueue(featurePath: string): Promise<QueueState> {
|
|
29
|
+
const pendingPath = join(featurePath, QUEUE_FILE);
|
|
30
|
+
const processedPath = join(featurePath, QUEUE_PROCESSED_FILE);
|
|
31
|
+
|
|
32
|
+
const warnings: string[] = [];
|
|
33
|
+
|
|
34
|
+
// Load pending items
|
|
35
|
+
const pending = await loadQueueFile(pendingPath, warnings);
|
|
36
|
+
|
|
37
|
+
// Load processed items
|
|
38
|
+
const processed = await loadQueueFile(processedPath, warnings);
|
|
39
|
+
|
|
40
|
+
// Build state
|
|
41
|
+
const state: QueueState = {
|
|
42
|
+
featurePath,
|
|
43
|
+
pending,
|
|
44
|
+
processed,
|
|
45
|
+
lastChecked: new Date().toISOString(),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Only include warnings if there are any
|
|
49
|
+
if (warnings.length > 0) {
|
|
50
|
+
state.warnings = warnings;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return state;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Load items from a queue file
|
|
58
|
+
*
|
|
59
|
+
* @param filePath - Path to the queue file
|
|
60
|
+
* @param warnings - Array to collect warnings for malformed lines
|
|
61
|
+
* @returns Array of QueueItems
|
|
62
|
+
*/
|
|
63
|
+
async function loadQueueFile(
|
|
64
|
+
filePath: string,
|
|
65
|
+
warnings: string[]
|
|
66
|
+
): Promise<QueueItem[]> {
|
|
67
|
+
const file = Bun.file(filePath);
|
|
68
|
+
|
|
69
|
+
// Handle missing file gracefully
|
|
70
|
+
if (!(await file.exists())) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const content = await file.text();
|
|
75
|
+
const lines = content.split("\n");
|
|
76
|
+
const items: QueueItem[] = [];
|
|
77
|
+
|
|
78
|
+
for (const line of lines) {
|
|
79
|
+
// Skip empty lines
|
|
80
|
+
const trimmed = line.trim();
|
|
81
|
+
if (!trimmed) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Try to parse the line
|
|
86
|
+
const item = parseQueueLine(trimmed);
|
|
87
|
+
|
|
88
|
+
if (item) {
|
|
89
|
+
items.push(item);
|
|
90
|
+
} else {
|
|
91
|
+
// Record warning for malformed line
|
|
92
|
+
warnings.push(`Skipped malformed line: ${trimmed}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return items;
|
|
97
|
+
}
|