@girardmedia/bootspring 1.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/LICENSE +21 -0
- package/README.md +255 -0
- package/agents/README.md +93 -0
- package/agents/api-expert/context.md +416 -0
- package/agents/architecture-expert/context.md +454 -0
- package/agents/backend-expert/context.md +483 -0
- package/agents/code-review-expert/context.md +365 -0
- package/agents/database-expert/context.md +250 -0
- package/agents/devops-expert/context.md +446 -0
- package/agents/frontend-expert/context.md +364 -0
- package/agents/index.js +140 -0
- package/agents/performance-expert/context.md +377 -0
- package/agents/security-expert/context.md +343 -0
- package/agents/testing-expert/context.md +414 -0
- package/agents/ui-ux-expert/context.md +448 -0
- package/agents/vercel-expert/context.md +426 -0
- package/bin/bootspring.js +310 -0
- package/cli/agent.js +337 -0
- package/cli/context.js +194 -0
- package/cli/dashboard.js +150 -0
- package/cli/generate.js +294 -0
- package/cli/init.js +410 -0
- package/cli/loop.js +421 -0
- package/cli/mcp.js +241 -0
- package/cli/memory.js +303 -0
- package/cli/orchestrator.js +400 -0
- package/cli/plugin.js +451 -0
- package/cli/quality.js +332 -0
- package/cli/skill.js +369 -0
- package/cli/task.js +628 -0
- package/cli/telemetry.js +114 -0
- package/cli/todo.js +614 -0
- package/cli/update.js +312 -0
- package/core/config.js +245 -0
- package/core/context.js +329 -0
- package/core/entitlements.js +209 -0
- package/core/index.js +43 -0
- package/core/policies.js +68 -0
- package/core/telemetry.js +247 -0
- package/core/utils.js +380 -0
- package/dashboard/server.js +818 -0
- package/docs/integrations/claude-code.md +42 -0
- package/docs/integrations/codex.md +42 -0
- package/docs/mcp-api-platform.md +102 -0
- package/generators/generate.js +598 -0
- package/generators/index.js +18 -0
- package/hooks/context-detector.js +177 -0
- package/hooks/index.js +35 -0
- package/hooks/prompt-enhancer.js +289 -0
- package/intelligence/git-memory.js +551 -0
- package/intelligence/index.js +59 -0
- package/intelligence/orchestrator.js +964 -0
- package/intelligence/prd.js +447 -0
- package/intelligence/recommendation-weights.json +18 -0
- package/intelligence/recommendations.js +234 -0
- package/mcp/capabilities.js +71 -0
- package/mcp/contracts/mcp-contract.v1.json +497 -0
- package/mcp/registry.js +213 -0
- package/mcp/response-formatter.js +462 -0
- package/mcp/server.js +99 -0
- package/mcp/tools/agent-tool.js +137 -0
- package/mcp/tools/capabilities-tool.js +54 -0
- package/mcp/tools/context-tool.js +49 -0
- package/mcp/tools/dashboard-tool.js +58 -0
- package/mcp/tools/generate-tool.js +46 -0
- package/mcp/tools/loop-tool.js +134 -0
- package/mcp/tools/memory-tool.js +180 -0
- package/mcp/tools/orchestrator-tool.js +232 -0
- package/mcp/tools/plugin-tool.js +76 -0
- package/mcp/tools/quality-tool.js +47 -0
- package/mcp/tools/skill-tool.js +233 -0
- package/mcp/tools/telemetry-tool.js +95 -0
- package/mcp/tools/todo-tool.js +133 -0
- package/package.json +98 -0
- package/plugins/index.js +141 -0
- package/quality/index.js +380 -0
- package/quality/lint-budgets.json +19 -0
- package/skills/index.js +787 -0
- package/skills/patterns/README.md +163 -0
- package/skills/patterns/api/route-handler.md +217 -0
- package/skills/patterns/api/server-action.md +249 -0
- package/skills/patterns/auth/clerk.md +132 -0
- package/skills/patterns/database/prisma.md +180 -0
- package/skills/patterns/payments/stripe.md +272 -0
- package/skills/patterns/security/validation.md +268 -0
- package/skills/patterns/testing/vitest.md +307 -0
- package/templates/bootspring.config.js +83 -0
- package/templates/mcp.json +9 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bootspring PRD (Product Requirements Document) Manager
|
|
5
|
+
*
|
|
6
|
+
* Manages structured task files for autonomous loop execution.
|
|
7
|
+
* Compatible with Ralph's prd.json format.
|
|
8
|
+
*
|
|
9
|
+
* @package bootspring
|
|
10
|
+
* @module intelligence/prd
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* PRD Schema
|
|
18
|
+
*
|
|
19
|
+
* {
|
|
20
|
+
* "name": "feature-name",
|
|
21
|
+
* "description": "Feature description",
|
|
22
|
+
* "created": "ISO timestamp",
|
|
23
|
+
* "stories": [
|
|
24
|
+
* {
|
|
25
|
+
* "id": "story-1",
|
|
26
|
+
* "title": "Short title",
|
|
27
|
+
* "description": "Detailed description",
|
|
28
|
+
* "acceptance": ["Criteria 1", "Criteria 2"],
|
|
29
|
+
* "priority": 1,
|
|
30
|
+
* "status": "pending|in_progress|complete|blocked",
|
|
31
|
+
* "completedAt": null | "ISO timestamp"
|
|
32
|
+
* }
|
|
33
|
+
* ]
|
|
34
|
+
* }
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const DEFAULT_PRD_DIR = 'tasks';
|
|
38
|
+
const DEFAULT_PRD_FILE = 'prd.json';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create a new PRD
|
|
42
|
+
*/
|
|
43
|
+
function createPRD(name, description, stories = []) {
|
|
44
|
+
return {
|
|
45
|
+
name,
|
|
46
|
+
description,
|
|
47
|
+
created: new Date().toISOString(),
|
|
48
|
+
updated: new Date().toISOString(),
|
|
49
|
+
stories: stories.map((story, index) => ({
|
|
50
|
+
id: story.id || `story-${index + 1}`,
|
|
51
|
+
title: story.title,
|
|
52
|
+
description: story.description || '',
|
|
53
|
+
acceptance: story.acceptance || [],
|
|
54
|
+
priority: story.priority || index + 1,
|
|
55
|
+
status: 'pending',
|
|
56
|
+
completedAt: null
|
|
57
|
+
}))
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Load PRD from file
|
|
63
|
+
*/
|
|
64
|
+
function loadPRD(filePath = path.join(DEFAULT_PRD_DIR, DEFAULT_PRD_FILE)) {
|
|
65
|
+
if (!fs.existsSync(filePath)) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
71
|
+
return JSON.parse(content);
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.error(`Error loading PRD: ${e.message}`);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Save PRD to file
|
|
80
|
+
*/
|
|
81
|
+
function savePRD(prd, filePath = path.join(DEFAULT_PRD_DIR, DEFAULT_PRD_FILE)) {
|
|
82
|
+
const dir = path.dirname(filePath);
|
|
83
|
+
if (!fs.existsSync(dir)) {
|
|
84
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
prd.updated = new Date().toISOString();
|
|
88
|
+
fs.writeFileSync(filePath, JSON.stringify(prd, null, 2));
|
|
89
|
+
return filePath;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get next incomplete story (highest priority)
|
|
94
|
+
*/
|
|
95
|
+
function getNextStory(prd) {
|
|
96
|
+
if (!prd || !prd.stories) return null;
|
|
97
|
+
|
|
98
|
+
return prd.stories
|
|
99
|
+
.filter(s => s.status === 'pending' || s.status === 'in_progress')
|
|
100
|
+
.sort((a, b) => a.priority - b.priority)[0] || null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Update story status
|
|
105
|
+
*/
|
|
106
|
+
function updateStoryStatus(prd, storyId, status) {
|
|
107
|
+
const story = prd.stories.find(s => s.id === storyId);
|
|
108
|
+
if (!story) {
|
|
109
|
+
throw new Error(`Story not found: ${storyId}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
story.status = status;
|
|
113
|
+
if (status === 'complete') {
|
|
114
|
+
story.completedAt = new Date().toISOString();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return prd;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get PRD progress summary
|
|
122
|
+
*/
|
|
123
|
+
function getProgress(prd) {
|
|
124
|
+
if (!prd || !prd.stories) {
|
|
125
|
+
return { total: 0, complete: 0, pending: 0, blocked: 0, percent: 0 };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const total = prd.stories.length;
|
|
129
|
+
const complete = prd.stories.filter(s => s.status === 'complete').length;
|
|
130
|
+
const pending = prd.stories.filter(s => s.status === 'pending' || s.status === 'in_progress').length;
|
|
131
|
+
const blocked = prd.stories.filter(s => s.status === 'blocked').length;
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
total,
|
|
135
|
+
complete,
|
|
136
|
+
pending,
|
|
137
|
+
blocked,
|
|
138
|
+
percent: total > 0 ? Math.round((complete / total) * 100) : 0
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Check if all stories are complete
|
|
144
|
+
*/
|
|
145
|
+
function isComplete(prd) {
|
|
146
|
+
if (!prd || !prd.stories || prd.stories.length === 0) return false;
|
|
147
|
+
return prd.stories.every(s => s.status === 'complete');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Parse markdown PRD into structured format
|
|
152
|
+
* Converts human-readable PRD to prd.json
|
|
153
|
+
*/
|
|
154
|
+
function parseMarkdownPRD(markdown) {
|
|
155
|
+
const lines = markdown.split('\n');
|
|
156
|
+
const stories = [];
|
|
157
|
+
|
|
158
|
+
let currentStory = null;
|
|
159
|
+
let inAcceptance = false;
|
|
160
|
+
let name = '';
|
|
161
|
+
let description = '';
|
|
162
|
+
let sawTitle = false;
|
|
163
|
+
|
|
164
|
+
for (const line of lines) {
|
|
165
|
+
// Extract name from H1
|
|
166
|
+
if (line.startsWith('# ')) {
|
|
167
|
+
name = line.replace('# ', '').trim();
|
|
168
|
+
sawTitle = true;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Extract top-level description (content after title and before first story heading)
|
|
173
|
+
if (sawTitle && !currentStory && line.trim() && !line.startsWith('#')) {
|
|
174
|
+
description += line.trim() + ' ';
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Acceptance criteria header (check BEFORE story header to avoid false matches)
|
|
179
|
+
if (line.match(/^#{3,4}\s+acceptance/i) || line.match(/^\*\*acceptance/i)) {
|
|
180
|
+
inAcceptance = true;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Story header supports both manual PRDs (## Story X) and exported markdown (### [ ] Story)
|
|
185
|
+
const storyMatch = line.match(/^#{2,3}\s+(?:\[[ x!]\]\s*)?(?:Story\s+)?(\d+)?[:.]?\s*(.+)/i);
|
|
186
|
+
if (storyMatch && !line.match(/acceptance|criteria|progress/i)) {
|
|
187
|
+
if (currentStory) {
|
|
188
|
+
stories.push(currentStory);
|
|
189
|
+
}
|
|
190
|
+
currentStory = {
|
|
191
|
+
id: `story-${stories.length + 1}`,
|
|
192
|
+
title: storyMatch[2].trim(),
|
|
193
|
+
description: '',
|
|
194
|
+
acceptance: [],
|
|
195
|
+
priority: stories.length + 1
|
|
196
|
+
};
|
|
197
|
+
inAcceptance = false;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Acceptance criteria items
|
|
202
|
+
if (inAcceptance && line.match(/^[-*]\s+/)) {
|
|
203
|
+
const criteria = line.replace(/^[-*]\s+/, '').trim();
|
|
204
|
+
if (criteria && currentStory) {
|
|
205
|
+
currentStory.acceptance.push(criteria);
|
|
206
|
+
}
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Story description (non-list content under story header)
|
|
211
|
+
if (currentStory && !inAcceptance && line.trim() && !line.startsWith('#')) {
|
|
212
|
+
currentStory.description += line.trim() + ' ';
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Don't forget last story
|
|
217
|
+
if (currentStory) {
|
|
218
|
+
stories.push(currentStory);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Clean up descriptions
|
|
222
|
+
stories.forEach(s => {
|
|
223
|
+
s.description = s.description.trim();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return createPRD(name, description.trim(), stories);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Generate markdown from PRD
|
|
231
|
+
*/
|
|
232
|
+
function toMarkdown(prd) {
|
|
233
|
+
let md = `# ${prd.name}\n\n`;
|
|
234
|
+
|
|
235
|
+
if (prd.description) {
|
|
236
|
+
md += `${prd.description}\n\n`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const progress = getProgress(prd);
|
|
240
|
+
md += `## Progress: ${progress.percent}% (${progress.complete}/${progress.total})\n\n`;
|
|
241
|
+
|
|
242
|
+
for (const story of prd.stories) {
|
|
243
|
+
const statusIcon = story.status === 'complete' ? '[x]' :
|
|
244
|
+
story.status === 'blocked' ? '[!]' : '[ ]';
|
|
245
|
+
|
|
246
|
+
md += `### ${statusIcon} ${story.title}\n\n`;
|
|
247
|
+
|
|
248
|
+
if (story.description) {
|
|
249
|
+
md += `${story.description}\n\n`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (story.acceptance.length > 0) {
|
|
253
|
+
md += `**Acceptance Criteria:**\n`;
|
|
254
|
+
for (const criteria of story.acceptance) {
|
|
255
|
+
md += `- ${criteria}\n`;
|
|
256
|
+
}
|
|
257
|
+
md += '\n';
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return md;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Validate story is right-sized for autonomous execution
|
|
266
|
+
* Stories should be completable in a single AI context window
|
|
267
|
+
*/
|
|
268
|
+
function validateStorySize(story) {
|
|
269
|
+
const warnings = [];
|
|
270
|
+
|
|
271
|
+
// Check title length (should be concise)
|
|
272
|
+
if (story.title.length > 100) {
|
|
273
|
+
warnings.push('Title too long - consider breaking down');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Check for scope indicators (use word boundaries to avoid false positives)
|
|
277
|
+
const scopeIndicators = [
|
|
278
|
+
/\bentire\b/i, /\ball\s+(the|of)\b/i, /\bcomplete\s+system\b/i,
|
|
279
|
+
/\bfull\s+(system|implementation)\b/i, /\bwhole\b/i,
|
|
280
|
+
/\bauthentication system\b/i, /\bpayment system\b/i, /\buser management\b/i,
|
|
281
|
+
/\bbuild\s+(the|entire|full)\b/i
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
const titleLower = story.title.toLowerCase();
|
|
285
|
+
const descLower = (story.description || '').toLowerCase();
|
|
286
|
+
const combined = `${titleLower} ${descLower}`;
|
|
287
|
+
|
|
288
|
+
for (const pattern of scopeIndicators) {
|
|
289
|
+
if (pattern.test(combined)) {
|
|
290
|
+
warnings.push(`Scope might be too large (matches pattern)`);
|
|
291
|
+
break; // Only add one scope warning
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Check acceptance criteria count
|
|
296
|
+
if (story.acceptance.length > 5) {
|
|
297
|
+
warnings.push('Many acceptance criteria - consider splitting');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
valid: warnings.length === 0,
|
|
302
|
+
warnings
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// CLI
|
|
307
|
+
if (require.main === module) {
|
|
308
|
+
const args = process.argv.slice(2);
|
|
309
|
+
const command = args[0];
|
|
310
|
+
|
|
311
|
+
switch (command) {
|
|
312
|
+
case 'create': {
|
|
313
|
+
const name = args[1] || 'feature';
|
|
314
|
+
const prd = createPRD(name, 'Created by bootspring', [
|
|
315
|
+
{ title: 'First task', description: 'Implement the first piece' },
|
|
316
|
+
{ title: 'Second task', description: 'Implement the second piece' }
|
|
317
|
+
]);
|
|
318
|
+
const filePath = savePRD(prd);
|
|
319
|
+
console.log(`Created PRD: ${filePath}`);
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
case 'status': {
|
|
324
|
+
const prd = loadPRD(args[1]);
|
|
325
|
+
if (!prd) {
|
|
326
|
+
console.error('PRD not found');
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
const progress = getProgress(prd);
|
|
330
|
+
console.log(`PRD: ${prd.name}`);
|
|
331
|
+
console.log(`Progress: ${progress.percent}% (${progress.complete}/${progress.total})`);
|
|
332
|
+
console.log(`Pending: ${progress.pending} | Blocked: ${progress.blocked}`);
|
|
333
|
+
|
|
334
|
+
const next = getNextStory(prd);
|
|
335
|
+
if (next) {
|
|
336
|
+
console.log(`\nNext story: ${next.title}`);
|
|
337
|
+
}
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
case 'next': {
|
|
342
|
+
const prd = loadPRD(args[1]);
|
|
343
|
+
if (!prd) {
|
|
344
|
+
console.error('PRD not found');
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
const next = getNextStory(prd);
|
|
348
|
+
if (next) {
|
|
349
|
+
console.log(JSON.stringify(next, null, 2));
|
|
350
|
+
} else {
|
|
351
|
+
console.log('No pending stories');
|
|
352
|
+
}
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
case 'complete': {
|
|
357
|
+
const storyId = args[1];
|
|
358
|
+
const prdPath = args[2] || path.join(DEFAULT_PRD_DIR, DEFAULT_PRD_FILE);
|
|
359
|
+
|
|
360
|
+
if (!storyId) {
|
|
361
|
+
console.error('Usage: prd.js complete <story-id> [prd-path]');
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const prd = loadPRD(prdPath);
|
|
366
|
+
if (!prd) {
|
|
367
|
+
console.error('PRD not found');
|
|
368
|
+
process.exit(1);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
updateStoryStatus(prd, storyId, 'complete');
|
|
372
|
+
savePRD(prd, prdPath);
|
|
373
|
+
console.log(`Marked ${storyId} as complete`);
|
|
374
|
+
|
|
375
|
+
if (isComplete(prd)) {
|
|
376
|
+
console.log('\nAll stories complete!');
|
|
377
|
+
}
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
case 'parse': {
|
|
382
|
+
const mdPath = args[1];
|
|
383
|
+
if (!mdPath || !fs.existsSync(mdPath)) {
|
|
384
|
+
console.error('Usage: prd.js parse <markdown-file>');
|
|
385
|
+
process.exit(1);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const markdown = fs.readFileSync(mdPath, 'utf-8');
|
|
389
|
+
const prd = parseMarkdownPRD(markdown);
|
|
390
|
+
|
|
391
|
+
// Validate stories
|
|
392
|
+
console.log(`Parsed PRD: ${prd.name}`);
|
|
393
|
+
console.log(`Stories: ${prd.stories.length}\n`);
|
|
394
|
+
|
|
395
|
+
for (const story of prd.stories) {
|
|
396
|
+
const validation = validateStorySize(story);
|
|
397
|
+
console.log(`- ${story.title}`);
|
|
398
|
+
if (!validation.valid) {
|
|
399
|
+
validation.warnings.forEach(w => console.log(` ⚠️ ${w}`));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Save
|
|
404
|
+
const outPath = savePRD(prd);
|
|
405
|
+
console.log(`\nSaved to: ${outPath}`);
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
case 'markdown': {
|
|
410
|
+
const prd = loadPRD(args[1]);
|
|
411
|
+
if (!prd) {
|
|
412
|
+
console.error('PRD not found');
|
|
413
|
+
process.exit(1);
|
|
414
|
+
}
|
|
415
|
+
console.log(toMarkdown(prd));
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
default:
|
|
420
|
+
console.log(`
|
|
421
|
+
Bootspring PRD Manager
|
|
422
|
+
|
|
423
|
+
Usage:
|
|
424
|
+
prd.js create [name] Create new PRD
|
|
425
|
+
prd.js status [path] Show PRD progress
|
|
426
|
+
prd.js next [path] Get next story as JSON
|
|
427
|
+
prd.js complete <id> [path] Mark story complete
|
|
428
|
+
prd.js parse <markdown> Convert markdown to prd.json
|
|
429
|
+
prd.js markdown [path] Convert prd.json to markdown
|
|
430
|
+
`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
module.exports = {
|
|
435
|
+
createPRD,
|
|
436
|
+
loadPRD,
|
|
437
|
+
savePRD,
|
|
438
|
+
getNextStory,
|
|
439
|
+
updateStoryStatus,
|
|
440
|
+
getProgress,
|
|
441
|
+
isComplete,
|
|
442
|
+
parseMarkdownPRD,
|
|
443
|
+
toMarkdown,
|
|
444
|
+
validateStorySize,
|
|
445
|
+
DEFAULT_PRD_DIR,
|
|
446
|
+
DEFAULT_PRD_FILE
|
|
447
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"workflow": {
|
|
3
|
+
"base": 10,
|
|
4
|
+
"completed": 6,
|
|
5
|
+
"started": 2,
|
|
6
|
+
"checkpointCap": 10,
|
|
7
|
+
"completionRateMultiplier": 20,
|
|
8
|
+
"contextMatch": 12,
|
|
9
|
+
"packBonus": 3,
|
|
10
|
+
"premiumUnlock": 4
|
|
11
|
+
},
|
|
12
|
+
"skill": {
|
|
13
|
+
"base": 25,
|
|
14
|
+
"usage": 4,
|
|
15
|
+
"premiumUnlock": 3
|
|
16
|
+
},
|
|
17
|
+
"eventWindowLimit": 1000
|
|
18
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recommendation engine v1
|
|
3
|
+
* Ranks workflows and skills from context + telemetry outcomes.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const DEFAULT_WEIGHTS_PATH = path.join(__dirname, 'recommendation-weights.json');
|
|
10
|
+
|
|
11
|
+
function tokenize(text) {
|
|
12
|
+
return String(text || '')
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.split(/[^a-z0-9]+/g)
|
|
15
|
+
.filter(Boolean);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function buildWorkflowStats(events) {
|
|
19
|
+
const stats = new Map();
|
|
20
|
+
|
|
21
|
+
for (const event of events) {
|
|
22
|
+
const workflow = event?.payload?.workflow;
|
|
23
|
+
if (!workflow) continue;
|
|
24
|
+
if (!stats.has(workflow)) {
|
|
25
|
+
stats.set(workflow, {
|
|
26
|
+
started: 0,
|
|
27
|
+
completed: 0,
|
|
28
|
+
checkpoints: 0,
|
|
29
|
+
premiumUnlocks: 0,
|
|
30
|
+
lastEventAt: null
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const current = stats.get(workflow);
|
|
34
|
+
if (event.event === 'workflow_started') current.started += 1;
|
|
35
|
+
if (event.event === 'workflow_completed') current.completed += 1;
|
|
36
|
+
if (event.event === 'workflow_checkpoint_completed' || event.event === 'pack_signal_checkpoint') {
|
|
37
|
+
current.checkpoints += 1;
|
|
38
|
+
}
|
|
39
|
+
if (event.event === 'premium_unlocked') {
|
|
40
|
+
current.premiumUnlocks += 1;
|
|
41
|
+
}
|
|
42
|
+
current.lastEventAt = event.timestamp || current.lastEventAt;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return stats;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function buildSkillUsage(events) {
|
|
49
|
+
const usage = new Map();
|
|
50
|
+
const premiumUnlocks = new Map();
|
|
51
|
+
for (const event of events) {
|
|
52
|
+
const skillId = event?.payload?.skillId;
|
|
53
|
+
if (!skillId) continue;
|
|
54
|
+
usage.set(skillId, (usage.get(skillId) || 0) + 1);
|
|
55
|
+
if (event.event === 'premium_unlocked') {
|
|
56
|
+
premiumUnlocks.set(skillId, (premiumUnlocks.get(skillId) || 0) + 1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { usage, premiumUnlocks };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function workflowMatchesContext(workflow, tokens) {
|
|
63
|
+
if (tokens.length === 0) return false;
|
|
64
|
+
const haystack = [
|
|
65
|
+
workflow.name,
|
|
66
|
+
workflow.description,
|
|
67
|
+
...(workflow.outcomes || []),
|
|
68
|
+
...(workflow.completionSignals || [])
|
|
69
|
+
]
|
|
70
|
+
.join(' ')
|
|
71
|
+
.toLowerCase();
|
|
72
|
+
|
|
73
|
+
return tokens.some(token => haystack.includes(token));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function mergeWeights(overrides = {}) {
|
|
77
|
+
let base = {};
|
|
78
|
+
try {
|
|
79
|
+
if (fs.existsSync(DEFAULT_WEIGHTS_PATH)) {
|
|
80
|
+
base = JSON.parse(fs.readFileSync(DEFAULT_WEIGHTS_PATH, 'utf-8'));
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
base = {};
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
workflow: {
|
|
87
|
+
...(base.workflow || {}),
|
|
88
|
+
...((overrides.workflow) || {})
|
|
89
|
+
},
|
|
90
|
+
skill: {
|
|
91
|
+
...(base.skill || {}),
|
|
92
|
+
...((overrides.skill) || {})
|
|
93
|
+
},
|
|
94
|
+
eventWindowLimit: Number(overrides.eventWindowLimit || base.eventWindowLimit || 1000)
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function resolveWeights(options = {}) {
|
|
99
|
+
if (options.weights && typeof options.weights === 'object') {
|
|
100
|
+
return mergeWeights(options.weights);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const customPath = options.weightsPath || process.env.BOOTSPRING_RECOMMENDATION_WEIGHTS_PATH;
|
|
104
|
+
if (customPath) {
|
|
105
|
+
try {
|
|
106
|
+
const raw = JSON.parse(fs.readFileSync(customPath, 'utf-8'));
|
|
107
|
+
return mergeWeights(raw);
|
|
108
|
+
} catch {
|
|
109
|
+
return mergeWeights({});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return mergeWeights({});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function createRecommendationsEngine({ intelligence, telemetry, skills, entitlements, weights }) {
|
|
117
|
+
const resolvedWeights = resolveWeights({ weights });
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
recommend({ contextText = '', limit = 5, accessOptions = {} } = {}) {
|
|
121
|
+
const parsedLimit = Number(limit);
|
|
122
|
+
const maxItems = Number.isFinite(parsedLimit) && parsedLimit > 0 ? parsedLimit : 5;
|
|
123
|
+
const events = telemetry.listEvents({ limit: resolvedWeights.eventWindowLimit });
|
|
124
|
+
const analysis = intelligence.analyzeContext(contextText || '');
|
|
125
|
+
const tokens = tokenize(contextText);
|
|
126
|
+
const workflowStats = buildWorkflowStats(events);
|
|
127
|
+
const skillStats = buildSkillUsage(events);
|
|
128
|
+
|
|
129
|
+
const available = intelligence.listWorkflows();
|
|
130
|
+
const accessible = entitlements.filterAccessibleWorkflows(available, accessOptions).allowed;
|
|
131
|
+
const workflows = accessible
|
|
132
|
+
.map(workflow => {
|
|
133
|
+
const stats = workflowStats.get(workflow.key) || {
|
|
134
|
+
started: 0,
|
|
135
|
+
completed: 0,
|
|
136
|
+
checkpoints: 0,
|
|
137
|
+
premiumUnlocks: 0,
|
|
138
|
+
lastEventAt: null
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const completionRate = stats.started > 0 ? (stats.completed / stats.started) : 0;
|
|
142
|
+
let score = Number(resolvedWeights.workflow.base || 10)
|
|
143
|
+
+ (stats.completed * Number(resolvedWeights.workflow.completed || 6))
|
|
144
|
+
+ (stats.started * Number(resolvedWeights.workflow.started || 2))
|
|
145
|
+
+ Math.min(Number(resolvedWeights.workflow.checkpointCap || 10), stats.checkpoints);
|
|
146
|
+
const reasons = [];
|
|
147
|
+
|
|
148
|
+
if (stats.completed > 0) reasons.push(`completed ${stats.completed} time(s)`);
|
|
149
|
+
if (completionRate > 0) {
|
|
150
|
+
score += Math.round(completionRate * Number(resolvedWeights.workflow.completionRateMultiplier || 20));
|
|
151
|
+
reasons.push(`completion rate ${(completionRate * 100).toFixed(0)}%`);
|
|
152
|
+
}
|
|
153
|
+
if (workflowMatchesContext(workflow, tokens)) {
|
|
154
|
+
score += Number(resolvedWeights.workflow.contextMatch || 12);
|
|
155
|
+
reasons.push('matches current context');
|
|
156
|
+
}
|
|
157
|
+
if (workflow.pack) {
|
|
158
|
+
score += Number(resolvedWeights.workflow.packBonus || 3);
|
|
159
|
+
}
|
|
160
|
+
if (stats.premiumUnlocks > 0) {
|
|
161
|
+
score += stats.premiumUnlocks * Number(resolvedWeights.workflow.premiumUnlock || 4);
|
|
162
|
+
reasons.push(`premium unlocks ${stats.premiumUnlocks}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
key: workflow.key,
|
|
167
|
+
name: workflow.name,
|
|
168
|
+
tier: workflow.tier || 'free',
|
|
169
|
+
pack: workflow.pack || null,
|
|
170
|
+
score,
|
|
171
|
+
reasons,
|
|
172
|
+
metrics: {
|
|
173
|
+
started: stats.started,
|
|
174
|
+
completed: stats.completed,
|
|
175
|
+
checkpoints: stats.checkpoints,
|
|
176
|
+
premiumUnlocks: stats.premiumUnlocks,
|
|
177
|
+
completionRate
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
})
|
|
181
|
+
.sort((a, b) => b.score - a.score)
|
|
182
|
+
.slice(0, maxItems);
|
|
183
|
+
|
|
184
|
+
const contextSkillIds = Array.from(new Set(analysis.skills || []));
|
|
185
|
+
let availableSkillIds = skills.listSkills({ includeExternal: true });
|
|
186
|
+
availableSkillIds = entitlements.filterAccessibleSkills(availableSkillIds, accessOptions).allowed;
|
|
187
|
+
const availableSkillSet = new Set(availableSkillIds);
|
|
188
|
+
|
|
189
|
+
const skillsRanked = contextSkillIds
|
|
190
|
+
.filter(skillId => availableSkillSet.has(skillId))
|
|
191
|
+
.map(skillId => {
|
|
192
|
+
const metadata = skills.getSkillMetadata(skillId) || {};
|
|
193
|
+
const usage = skillStats.usage.get(skillId) || 0;
|
|
194
|
+
const premiumUnlocks = skillStats.premiumUnlocks.get(skillId) || 0;
|
|
195
|
+
return {
|
|
196
|
+
id: skillId,
|
|
197
|
+
name: metadata.name || skillId,
|
|
198
|
+
description: metadata.description || '',
|
|
199
|
+
source: metadata.source || 'built-in',
|
|
200
|
+
score: Number(resolvedWeights.skill.base || 25)
|
|
201
|
+
+ (usage * Number(resolvedWeights.skill.usage || 4))
|
|
202
|
+
+ (premiumUnlocks * Number(resolvedWeights.skill.premiumUnlock || 3)),
|
|
203
|
+
reasons: [
|
|
204
|
+
'suggested by context analysis',
|
|
205
|
+
...(usage > 0 ? [`used ${usage} time(s) in prior sessions`] : []),
|
|
206
|
+
...(premiumUnlocks > 0 ? [`premium unlocks ${premiumUnlocks}`] : [])
|
|
207
|
+
]
|
|
208
|
+
};
|
|
209
|
+
})
|
|
210
|
+
.sort((a, b) => b.score - a.score)
|
|
211
|
+
.slice(0, maxItems);
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
context: {
|
|
215
|
+
phase: analysis.phase,
|
|
216
|
+
phaseName: analysis.phaseConfig?.name,
|
|
217
|
+
suggestions: analysis.suggestions
|
|
218
|
+
},
|
|
219
|
+
workflows,
|
|
220
|
+
skills: skillsRanked,
|
|
221
|
+
telemetryWindow: {
|
|
222
|
+
eventsAnalyzed: events.length
|
|
223
|
+
},
|
|
224
|
+
weights: resolvedWeights
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
module.exports = {
|
|
231
|
+
createRecommendationsEngine,
|
|
232
|
+
resolveWeights,
|
|
233
|
+
mergeWeights
|
|
234
|
+
};
|