@arvorco/relentless 0.4.5 → 0.5.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/skills/constitution/templates/prompt.md +35 -45
- package/.claude/skills/convert/SKILL.md +55 -6
- package/.claude/skills/tasks/SKILL.md +26 -0
- package/CHANGELOG.md +31 -3
- package/bin/relentless.ts +124 -2
- package/package.json +1 -1
- package/src/execution/context-builder.ts +417 -0
- package/src/execution/index.ts +1 -0
- package/src/execution/runner.ts +61 -16
- package/src/prd/index.ts +1 -0
- package/src/prd/parser.ts +305 -13
- package/src/prd/validator.ts +553 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Builder - Task-Specific Context Extraction
|
|
3
|
+
*
|
|
4
|
+
* Optimizes token usage by extracting only relevant context for the current story
|
|
5
|
+
* instead of loading entire files wholesale.
|
|
6
|
+
*
|
|
7
|
+
* This module provides:
|
|
8
|
+
* - extractStoryFromTasks: Extract current story + dependencies from tasks.md
|
|
9
|
+
* - filterChecklistForStory: Filter checklist to story-specific items
|
|
10
|
+
* - extractStoryMetadata: Extract current story metadata from PRD
|
|
11
|
+
* - buildProgressSummary: Build concise progress summary
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
15
|
+
import type { PRD, UserStory } from "../prd/types";
|
|
16
|
+
import { countStories } from "../prd/types";
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Types
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Context extracted from tasks.md for a specific story
|
|
24
|
+
*/
|
|
25
|
+
export interface StoryContext {
|
|
26
|
+
/** Markdown content for the current story section */
|
|
27
|
+
currentStory: string;
|
|
28
|
+
/** Markdown content for dependency story sections */
|
|
29
|
+
dependencies: string[];
|
|
30
|
+
/** Concise progress summary (e.g., "Progress: 5/18 stories complete") */
|
|
31
|
+
progressSummary: string;
|
|
32
|
+
/** Story statistics from PRD */
|
|
33
|
+
stats: {
|
|
34
|
+
total: number;
|
|
35
|
+
completed: number;
|
|
36
|
+
pending: number;
|
|
37
|
+
skipped: number;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Checklist items filtered for a specific story
|
|
43
|
+
*/
|
|
44
|
+
export interface FilteredChecklist {
|
|
45
|
+
/** Items tagged with the specific story ID [US-XXX] */
|
|
46
|
+
storyItems: string[];
|
|
47
|
+
/** Items tagged [Constitution] - always relevant */
|
|
48
|
+
constitutionItems: string[];
|
|
49
|
+
/** Items tagged [Edge Case], [Gap], [Clarified] - general relevance */
|
|
50
|
+
generalItems: string[];
|
|
51
|
+
/** Total item count in the original checklist */
|
|
52
|
+
totalItemCount: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Metadata extracted for a specific story from PRD
|
|
57
|
+
*/
|
|
58
|
+
export interface StoryMetadata {
|
|
59
|
+
id: string;
|
|
60
|
+
title: string;
|
|
61
|
+
description: string;
|
|
62
|
+
acceptanceCriteria: string[];
|
|
63
|
+
priority: number;
|
|
64
|
+
dependencies: string[];
|
|
65
|
+
phase?: string;
|
|
66
|
+
research?: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Story Extraction from tasks.md
|
|
71
|
+
// ============================================================================
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Extract story section from tasks.md content
|
|
75
|
+
*
|
|
76
|
+
* Stories are identified by the pattern: ### US-XXX: Title
|
|
77
|
+
* The section ends at the next story header or section divider (---)
|
|
78
|
+
*/
|
|
79
|
+
function extractStorySectionFromContent(
|
|
80
|
+
content: string,
|
|
81
|
+
storyId: string
|
|
82
|
+
): string {
|
|
83
|
+
const lines = content.split("\n");
|
|
84
|
+
const storyPattern = new RegExp(`^###\\s+${storyId}:`, "i");
|
|
85
|
+
|
|
86
|
+
let inStory = false;
|
|
87
|
+
let storyLines: string[] = [];
|
|
88
|
+
|
|
89
|
+
for (const line of lines) {
|
|
90
|
+
// Check if we're starting the target story
|
|
91
|
+
if (storyPattern.test(line)) {
|
|
92
|
+
inStory = true;
|
|
93
|
+
storyLines = [line];
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (inStory) {
|
|
98
|
+
// Check if we've hit the next story or section divider
|
|
99
|
+
if (/^###\s+US-\d+:/.test(line) || /^---$/.test(line)) {
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
storyLines.push(line);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return storyLines.join("\n").trim();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Extract a story section and its dependencies from tasks.md
|
|
111
|
+
*
|
|
112
|
+
* @param tasksPath - Path to tasks.md file
|
|
113
|
+
* @param currentStoryId - ID of the current story (e.g., "US-002")
|
|
114
|
+
* @param prd - PRD object with all story metadata including dependencies
|
|
115
|
+
* @returns StoryContext with current story, dependencies, and stats
|
|
116
|
+
*/
|
|
117
|
+
export async function extractStoryFromTasks(
|
|
118
|
+
tasksPath: string,
|
|
119
|
+
currentStoryId: string,
|
|
120
|
+
prd: PRD
|
|
121
|
+
): Promise<StoryContext> {
|
|
122
|
+
// Get stats from PRD
|
|
123
|
+
const stats = countStories(prd);
|
|
124
|
+
const progressSummary = buildProgressSummary(prd);
|
|
125
|
+
|
|
126
|
+
// Handle missing file
|
|
127
|
+
if (!existsSync(tasksPath)) {
|
|
128
|
+
return {
|
|
129
|
+
currentStory: "",
|
|
130
|
+
dependencies: [],
|
|
131
|
+
progressSummary,
|
|
132
|
+
stats,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const content = await Bun.file(tasksPath).text();
|
|
137
|
+
|
|
138
|
+
// Extract current story section
|
|
139
|
+
const currentStory = extractStorySectionFromContent(content, currentStoryId);
|
|
140
|
+
|
|
141
|
+
// Find the story in PRD to get dependencies
|
|
142
|
+
const story = prd.userStories.find((s) => s.id === currentStoryId);
|
|
143
|
+
const dependencyIds = story?.dependencies ?? [];
|
|
144
|
+
|
|
145
|
+
// Extract dependency story sections
|
|
146
|
+
const dependencies: string[] = [];
|
|
147
|
+
for (const depId of dependencyIds) {
|
|
148
|
+
const depSection = extractStorySectionFromContent(content, depId);
|
|
149
|
+
if (depSection) {
|
|
150
|
+
dependencies.push(depSection);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
currentStory,
|
|
156
|
+
dependencies,
|
|
157
|
+
progressSummary,
|
|
158
|
+
stats,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ============================================================================
|
|
163
|
+
// Checklist Filtering
|
|
164
|
+
// ============================================================================
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Filter checklist items for a specific story
|
|
168
|
+
*
|
|
169
|
+
* Extracts:
|
|
170
|
+
* - Items tagged with the story ID [US-XXX]
|
|
171
|
+
* - Items tagged [Constitution] (always relevant)
|
|
172
|
+
* - Items tagged [Edge Case], [Gap], [Clarified] (general relevance)
|
|
173
|
+
*
|
|
174
|
+
* @param checklistPath - Path to checklist.md file
|
|
175
|
+
* @param storyId - ID of the current story (e.g., "US-002")
|
|
176
|
+
* @returns FilteredChecklist with categorized items
|
|
177
|
+
*/
|
|
178
|
+
export async function filterChecklistForStory(
|
|
179
|
+
checklistPath: string,
|
|
180
|
+
storyId: string
|
|
181
|
+
): Promise<FilteredChecklist> {
|
|
182
|
+
// Handle missing file
|
|
183
|
+
if (!existsSync(checklistPath)) {
|
|
184
|
+
return {
|
|
185
|
+
storyItems: [],
|
|
186
|
+
constitutionItems: [],
|
|
187
|
+
generalItems: [],
|
|
188
|
+
totalItemCount: 0,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const content = await Bun.file(checklistPath).text();
|
|
193
|
+
const lines = content.split("\n");
|
|
194
|
+
|
|
195
|
+
const storyItems: string[] = [];
|
|
196
|
+
const constitutionItems: string[] = [];
|
|
197
|
+
const generalItems: string[] = [];
|
|
198
|
+
let totalItemCount = 0;
|
|
199
|
+
|
|
200
|
+
// Pattern for checklist items: - [ ] CHK-XXX [TAG] Description
|
|
201
|
+
// or: - [x] CHK-XXX [TAG] Description
|
|
202
|
+
const checklistItemPattern = /^-\s+\[[ x]\]\s+CHK-\d+/;
|
|
203
|
+
|
|
204
|
+
for (const line of lines) {
|
|
205
|
+
const trimmed = line.trim();
|
|
206
|
+
|
|
207
|
+
// Check if it's a checklist item
|
|
208
|
+
if (checklistItemPattern.test(trimmed)) {
|
|
209
|
+
totalItemCount++;
|
|
210
|
+
|
|
211
|
+
// Categorize by tag
|
|
212
|
+
if (trimmed.includes(`[${storyId}]`)) {
|
|
213
|
+
storyItems.push(trimmed);
|
|
214
|
+
} else if (trimmed.includes("[Constitution]")) {
|
|
215
|
+
constitutionItems.push(trimmed);
|
|
216
|
+
} else if (
|
|
217
|
+
trimmed.includes("[Edge Case]") ||
|
|
218
|
+
trimmed.includes("[Gap]") ||
|
|
219
|
+
trimmed.includes("[Clarified]")
|
|
220
|
+
) {
|
|
221
|
+
generalItems.push(trimmed);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
storyItems,
|
|
228
|
+
constitutionItems,
|
|
229
|
+
generalItems,
|
|
230
|
+
totalItemCount,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ============================================================================
|
|
235
|
+
// Story Metadata Extraction
|
|
236
|
+
// ============================================================================
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Extract metadata for a specific story from PRD
|
|
240
|
+
*
|
|
241
|
+
* @param prd - PRD object with all stories
|
|
242
|
+
* @param storyId - ID of the story to extract
|
|
243
|
+
* @returns StoryMetadata or null if story not found
|
|
244
|
+
*/
|
|
245
|
+
export function extractStoryMetadata(
|
|
246
|
+
prd: PRD,
|
|
247
|
+
storyId: string
|
|
248
|
+
): StoryMetadata | null {
|
|
249
|
+
const story = prd.userStories.find((s) => s.id === storyId);
|
|
250
|
+
|
|
251
|
+
if (!story) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
id: story.id,
|
|
257
|
+
title: story.title,
|
|
258
|
+
description: story.description,
|
|
259
|
+
acceptanceCriteria: story.acceptanceCriteria,
|
|
260
|
+
priority: story.priority,
|
|
261
|
+
dependencies: story.dependencies ?? [],
|
|
262
|
+
phase: story.phase,
|
|
263
|
+
research: story.research,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ============================================================================
|
|
268
|
+
// Progress Summary
|
|
269
|
+
// ============================================================================
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Build a concise progress summary from PRD
|
|
273
|
+
*
|
|
274
|
+
* @param prd - PRD object with all stories
|
|
275
|
+
* @returns Progress summary string (e.g., "Progress: 5/18 stories complete")
|
|
276
|
+
*/
|
|
277
|
+
export function buildProgressSummary(prd: PRD): string {
|
|
278
|
+
const stats = countStories(prd);
|
|
279
|
+
return `Progress: ${stats.completed}/${stats.total} stories complete`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ============================================================================
|
|
283
|
+
// Optimized Context Building (for runner.ts integration)
|
|
284
|
+
// ============================================================================
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Options for building an optimized prompt
|
|
288
|
+
*/
|
|
289
|
+
export interface OptimizedContextOptions {
|
|
290
|
+
/** Path to the feature directory */
|
|
291
|
+
featureDir: string;
|
|
292
|
+
/** Current story being worked on */
|
|
293
|
+
story: UserStory;
|
|
294
|
+
/** Full PRD object */
|
|
295
|
+
prd: PRD;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Optimized context components extracted for prompt building
|
|
300
|
+
*/
|
|
301
|
+
export interface OptimizedContext {
|
|
302
|
+
/** Story context from tasks.md */
|
|
303
|
+
storyContext: StoryContext;
|
|
304
|
+
/** Filtered checklist items */
|
|
305
|
+
checklist: FilteredChecklist;
|
|
306
|
+
/** Story metadata from PRD */
|
|
307
|
+
metadata: StoryMetadata | null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Build optimized context for a story
|
|
312
|
+
*
|
|
313
|
+
* Extracts only the relevant portions of tasks.md and checklist.md
|
|
314
|
+
* instead of loading the entire files.
|
|
315
|
+
*
|
|
316
|
+
* @param options - Context building options
|
|
317
|
+
* @returns OptimizedContext with all extracted components
|
|
318
|
+
*/
|
|
319
|
+
export async function buildOptimizedContext(
|
|
320
|
+
options: OptimizedContextOptions
|
|
321
|
+
): Promise<OptimizedContext> {
|
|
322
|
+
const { featureDir, story, prd } = options;
|
|
323
|
+
|
|
324
|
+
// Build paths
|
|
325
|
+
const tasksPath = `${featureDir}/tasks.md`;
|
|
326
|
+
const checklistPath = `${featureDir}/checklist.md`;
|
|
327
|
+
|
|
328
|
+
// Extract context in parallel
|
|
329
|
+
const [storyContext, checklist] = await Promise.all([
|
|
330
|
+
extractStoryFromTasks(tasksPath, story.id, prd),
|
|
331
|
+
filterChecklistForStory(checklistPath, story.id),
|
|
332
|
+
]);
|
|
333
|
+
|
|
334
|
+
// Extract metadata
|
|
335
|
+
const metadata = extractStoryMetadata(prd, story.id);
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
storyContext,
|
|
339
|
+
checklist,
|
|
340
|
+
metadata,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Format story context for inclusion in prompt
|
|
346
|
+
*
|
|
347
|
+
* @param context - Story context from extractStoryFromTasks
|
|
348
|
+
* @returns Formatted markdown string
|
|
349
|
+
*/
|
|
350
|
+
export function formatStoryContext(context: StoryContext): string {
|
|
351
|
+
if (!context.currentStory) {
|
|
352
|
+
return "";
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
let formatted = `\n\n## Current Story Context\n\n`;
|
|
356
|
+
formatted += `${context.progressSummary}\n\n`;
|
|
357
|
+
formatted += context.currentStory;
|
|
358
|
+
|
|
359
|
+
if (context.dependencies.length > 0) {
|
|
360
|
+
formatted += `\n\n### Dependency Stories (for reference)\n\n`;
|
|
361
|
+
for (const dep of context.dependencies) {
|
|
362
|
+
formatted += `${dep}\n\n`;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return formatted;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Format filtered checklist for inclusion in prompt
|
|
371
|
+
*
|
|
372
|
+
* @param checklist - Filtered checklist from filterChecklistForStory
|
|
373
|
+
* @param storyId - Story ID for header
|
|
374
|
+
* @returns Formatted markdown string
|
|
375
|
+
*/
|
|
376
|
+
export function formatFilteredChecklist(
|
|
377
|
+
checklist: FilteredChecklist,
|
|
378
|
+
storyId: string
|
|
379
|
+
): string {
|
|
380
|
+
const hasItems =
|
|
381
|
+
checklist.storyItems.length > 0 ||
|
|
382
|
+
checklist.constitutionItems.length > 0 ||
|
|
383
|
+
checklist.generalItems.length > 0;
|
|
384
|
+
|
|
385
|
+
if (!hasItems) {
|
|
386
|
+
return "";
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
let formatted = `\n\n## Relevant Quality Checklist Items\n\n`;
|
|
390
|
+
formatted += `Filtered from ${checklist.totalItemCount} total items:\n\n`;
|
|
391
|
+
|
|
392
|
+
if (checklist.storyItems.length > 0) {
|
|
393
|
+
formatted += `### Story-Specific Items (${storyId})\n\n`;
|
|
394
|
+
for (const item of checklist.storyItems) {
|
|
395
|
+
formatted += `${item}\n`;
|
|
396
|
+
}
|
|
397
|
+
formatted += "\n";
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (checklist.constitutionItems.length > 0) {
|
|
401
|
+
formatted += `### Constitution Items (Always Required)\n\n`;
|
|
402
|
+
for (const item of checklist.constitutionItems) {
|
|
403
|
+
formatted += `${item}\n`;
|
|
404
|
+
}
|
|
405
|
+
formatted += "\n";
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (checklist.generalItems.length > 0) {
|
|
409
|
+
formatted += `### General Quality Items\n\n`;
|
|
410
|
+
for (const item of checklist.generalItems) {
|
|
411
|
+
formatted += `${item}\n`;
|
|
412
|
+
}
|
|
413
|
+
formatted += "\n";
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return formatted;
|
|
417
|
+
}
|
package/src/execution/index.ts
CHANGED
package/src/execution/runner.ts
CHANGED
|
@@ -17,6 +17,13 @@ import { loadProgress, updateProgressMetadata, syncPatternsFromContent, appendPr
|
|
|
17
17
|
import { routeTask, type RoutingDecision } from "../routing/router";
|
|
18
18
|
import { getModelForHarnessAndMode, getFreeModeHarnesses } from "../routing/fallback";
|
|
19
19
|
import { buildStoryPromptAddition } from "./story-prompt";
|
|
20
|
+
import {
|
|
21
|
+
extractStoryFromTasks,
|
|
22
|
+
filterChecklistForStory,
|
|
23
|
+
formatStoryContext,
|
|
24
|
+
formatFilteredChecklist,
|
|
25
|
+
} from "./context-builder";
|
|
26
|
+
import type { PRD } from "../prd/types";
|
|
20
27
|
import { processQueue } from "../queue";
|
|
21
28
|
import type { QueueProcessResult } from "../queue/types";
|
|
22
29
|
import {
|
|
@@ -184,12 +191,19 @@ export function formatQueueLogMessage(promptCount: number, commandCount: number)
|
|
|
184
191
|
|
|
185
192
|
/**
|
|
186
193
|
* Build the prompt for an iteration
|
|
194
|
+
*
|
|
195
|
+
* @param promptPath - Path to prompt.md
|
|
196
|
+
* @param workingDirectory - Working directory
|
|
197
|
+
* @param progressPath - Path to progress.txt
|
|
198
|
+
* @param story - Current story (optional)
|
|
199
|
+
* @param prd - PRD object for optimized context extraction (optional)
|
|
187
200
|
*/
|
|
188
201
|
async function buildPrompt(
|
|
189
202
|
promptPath: string,
|
|
190
203
|
workingDirectory: string,
|
|
191
204
|
progressPath: string,
|
|
192
|
-
story?: UserStory
|
|
205
|
+
story?: UserStory,
|
|
206
|
+
prd?: PRD
|
|
193
207
|
): Promise<string> {
|
|
194
208
|
if (!existsSync(promptPath)) {
|
|
195
209
|
throw new Error(`Prompt file not found: ${promptPath}`);
|
|
@@ -243,25 +257,56 @@ async function buildPrompt(
|
|
|
243
257
|
prompt += planContent;
|
|
244
258
|
}
|
|
245
259
|
|
|
246
|
-
// Load
|
|
260
|
+
// Load tasks.md - optimized extraction when story and PRD are available
|
|
247
261
|
const tasksPath = join(dirname(progressPath), "tasks.md");
|
|
248
262
|
if (existsSync(tasksPath)) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
263
|
+
if (story && prd) {
|
|
264
|
+
// OPTIMIZED: Extract only current story and dependencies (~84% token savings)
|
|
265
|
+
const storyContext = await extractStoryFromTasks(tasksPath, story.id, prd);
|
|
266
|
+
if (storyContext.currentStory) {
|
|
267
|
+
prompt += `\n\n## User Stories and Tasks (Optimized Context)\n\n`;
|
|
268
|
+
prompt += `${storyContext.progressSummary}\n\n`;
|
|
269
|
+
prompt += `**IMPORTANT:** Update the checkboxes in tasks.md as you complete each criterion.\n`;
|
|
270
|
+
prompt += `Change \`- [ ]\` to \`- [x]\` for completed items.\n\n`;
|
|
271
|
+
prompt += formatStoryContext(storyContext);
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
// FALLBACK: Load full tasks.md when no story/PRD context available
|
|
275
|
+
const tasksContent = await Bun.file(tasksPath).text();
|
|
276
|
+
prompt += `\n\n## User Stories and Tasks\n\n`;
|
|
277
|
+
prompt += `The following tasks file contains all user stories with their acceptance criteria.\n`;
|
|
278
|
+
prompt += `**IMPORTANT:** Update the checkboxes in this file as you complete each criterion.\n`;
|
|
279
|
+
prompt += `Change \`- [ ]\` to \`- [x]\` for completed items.\n\n`;
|
|
280
|
+
prompt += tasksContent;
|
|
281
|
+
}
|
|
255
282
|
}
|
|
256
283
|
|
|
257
|
-
// Load
|
|
284
|
+
// Load checklist.md - optimized filtering when story is available
|
|
258
285
|
const checklistPath = join(dirname(progressPath), "checklist.md");
|
|
259
286
|
if (existsSync(checklistPath)) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
287
|
+
if (story) {
|
|
288
|
+
// OPTIMIZED: Filter to story-specific items only (~80% token savings)
|
|
289
|
+
const filteredChecklist = await filterChecklistForStory(checklistPath, story.id);
|
|
290
|
+
const hasRelevantItems =
|
|
291
|
+
filteredChecklist.storyItems.length > 0 ||
|
|
292
|
+
filteredChecklist.constitutionItems.length > 0 ||
|
|
293
|
+
filteredChecklist.generalItems.length > 0;
|
|
294
|
+
|
|
295
|
+
if (hasRelevantItems) {
|
|
296
|
+
prompt += `\n\n## Quality Checklist (Filtered for ${story.id})\n\n`;
|
|
297
|
+
prompt += `Showing relevant items from ${filteredChecklist.totalItemCount} total checklist items.\n`;
|
|
298
|
+
prompt += `Verify these items as you implement. Update checklist.md to mark items as checked.\n`;
|
|
299
|
+
prompt += formatFilteredChecklist(filteredChecklist, story.id);
|
|
300
|
+
prompt += `\nIMPORTANT: Review these checklist items and verify all applicable ones.\n`;
|
|
301
|
+
}
|
|
302
|
+
} else {
|
|
303
|
+
// FALLBACK: Load full checklist.md when no story context available
|
|
304
|
+
const checklistContent = await Bun.file(checklistPath).text();
|
|
305
|
+
prompt += `\n\n## Quality Checklist\n\n`;
|
|
306
|
+
prompt += `The following quality checks must be validated before marking stories as complete:\n\n`;
|
|
307
|
+
prompt += checklistContent;
|
|
308
|
+
prompt += `\n\nIMPORTANT: Review this checklist after implementing each story and verify all applicable items.\n`;
|
|
309
|
+
}
|
|
265
310
|
}
|
|
266
311
|
|
|
267
312
|
// Load and append research findings if available
|
|
@@ -680,7 +725,7 @@ export async function run(options: RunOptions): Promise<RunResult> {
|
|
|
680
725
|
mkdirSync(researchDir, { recursive: true });
|
|
681
726
|
}
|
|
682
727
|
|
|
683
|
-
let researchPrompt = await buildPrompt(options.promptPath, options.workingDirectory, progressPath, story);
|
|
728
|
+
let researchPrompt = await buildPrompt(options.promptPath, options.workingDirectory, progressPath, story, currentPRD);
|
|
684
729
|
|
|
685
730
|
// Inject queue prompts into research phase too
|
|
686
731
|
if (queuePrompts.length > 0) {
|
|
@@ -737,7 +782,7 @@ export async function run(options: RunOptions): Promise<RunResult> {
|
|
|
737
782
|
console.log(chalk.cyan(" 🔨 Implementation phase - applying research findings..."));
|
|
738
783
|
}
|
|
739
784
|
|
|
740
|
-
let prompt = await buildPrompt(options.promptPath, options.workingDirectory, progressPath, story);
|
|
785
|
+
let prompt = await buildPrompt(options.promptPath, options.workingDirectory, progressPath, story, currentPRD);
|
|
741
786
|
|
|
742
787
|
// Inject queue prompts if any
|
|
743
788
|
if (queuePrompts.length > 0) {
|
package/src/prd/index.ts
CHANGED