hone-ai 0.2.0 → 0.9.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/README.md +47 -2
- package/package.json +8 -5
- package/src/agent-client.integration.test.ts +57 -59
- package/src/agent-client.test.ts +27 -27
- package/src/agent-client.ts +109 -77
- package/src/agent.test.ts +16 -16
- package/src/agent.ts +103 -103
- package/src/agents-md-generator.test.ts +360 -0
- package/src/agents-md-generator.ts +900 -0
- package/src/config.test.ts +209 -224
- package/src/config.ts +84 -83
- package/src/errors.test.ts +211 -208
- package/src/errors.ts +107 -101
- package/src/index.integration.test.ts +327 -223
- package/src/index.ts +163 -100
- package/src/integration-test.ts +168 -137
- package/src/logger.test.ts +67 -67
- package/src/logger.ts +8 -8
- package/src/prd-generator.integration.test.ts +50 -50
- package/src/prd-generator.test.ts +66 -25
- package/src/prd-generator.ts +280 -194
- package/src/prds.test.ts +60 -65
- package/src/prds.ts +64 -62
- package/src/prompt.test.ts +154 -155
- package/src/prompt.ts +63 -65
- package/src/run.ts +147 -147
- package/src/status.test.ts +80 -80
- package/src/status.ts +40 -42
- package/src/task-generator.test.ts +93 -66
- package/src/task-generator.ts +125 -112
package/src/prd-generator.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { loadConfig, resolveModelForPhase } from './config'
|
|
2
|
-
import { readFile, writeFile, readdir } from 'fs/promises'
|
|
3
|
-
import { join } from 'path'
|
|
4
|
-
import { existsSync } from 'fs'
|
|
5
|
-
import * as readline from 'readline'
|
|
6
|
-
import { exitWithError, ErrorMessages } from './errors'
|
|
7
|
-
import { AgentClient } from './agent-client'
|
|
1
|
+
import { loadConfig, resolveModelForPhase } from './config'
|
|
2
|
+
import { readFile, writeFile, readdir } from 'fs/promises'
|
|
3
|
+
import { join } from 'path'
|
|
4
|
+
import { existsSync } from 'fs'
|
|
5
|
+
import * as readline from 'readline'
|
|
6
|
+
import { exitWithError, ErrorMessages } from './errors'
|
|
7
|
+
import { AgentClient } from './agent-client'
|
|
8
8
|
|
|
9
9
|
export function slugify(text: string): string {
|
|
10
10
|
return text
|
|
@@ -14,58 +14,58 @@ export function slugify(text: string): string {
|
|
|
14
14
|
.replace(/\s+/g, '-')
|
|
15
15
|
.replace(/-+/g, '-')
|
|
16
16
|
.replace(/^-+|-+$/g, '')
|
|
17
|
-
.slice(0, 50)
|
|
17
|
+
.slice(0, 50)
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
async function analyzeCodebase(): Promise<string> {
|
|
21
|
-
const analysis: string[] = []
|
|
22
|
-
const projectRoot = process.cwd()
|
|
23
|
-
|
|
21
|
+
const analysis: string[] = []
|
|
22
|
+
const projectRoot = process.cwd()
|
|
23
|
+
|
|
24
24
|
// Package.json analysis
|
|
25
25
|
if (existsSync(join(projectRoot, 'package.json'))) {
|
|
26
|
-
const pkg = JSON.parse(await readFile(join(projectRoot, 'package.json'), 'utf-8'))
|
|
27
|
-
analysis.push(`Project: ${pkg.name || 'unnamed'}`)
|
|
28
|
-
if (pkg.description) analysis.push(`Description: ${pkg.description}`)
|
|
29
|
-
|
|
26
|
+
const pkg = JSON.parse(await readFile(join(projectRoot, 'package.json'), 'utf-8'))
|
|
27
|
+
analysis.push(`Project: ${pkg.name || 'unnamed'}`)
|
|
28
|
+
if (pkg.description) analysis.push(`Description: ${pkg.description}`)
|
|
29
|
+
|
|
30
30
|
if (pkg.dependencies) {
|
|
31
|
-
const deps = Object.keys(pkg.dependencies)
|
|
32
|
-
if (deps.includes('react')) analysis.push('Framework: React')
|
|
33
|
-
if (deps.includes('next')) analysis.push('Framework: Next.js')
|
|
34
|
-
if (deps.includes('vue')) analysis.push('Framework: Vue')
|
|
35
|
-
if (deps.includes('typescript')) analysis.push('Language: TypeScript')
|
|
36
|
-
if (deps.includes('commander')) analysis.push('CLI: commander.js')
|
|
37
|
-
if (deps.includes('express')) analysis.push('Backend: Express')
|
|
38
|
-
if (deps.includes('fastify')) analysis.push('Backend: Fastify')
|
|
39
|
-
|
|
31
|
+
const deps = Object.keys(pkg.dependencies)
|
|
32
|
+
if (deps.includes('react')) analysis.push('Framework: React')
|
|
33
|
+
if (deps.includes('next')) analysis.push('Framework: Next.js')
|
|
34
|
+
if (deps.includes('vue')) analysis.push('Framework: Vue')
|
|
35
|
+
if (deps.includes('typescript')) analysis.push('Language: TypeScript')
|
|
36
|
+
if (deps.includes('commander')) analysis.push('CLI: commander.js')
|
|
37
|
+
if (deps.includes('express')) analysis.push('Backend: Express')
|
|
38
|
+
if (deps.includes('fastify')) analysis.push('Backend: Fastify')
|
|
39
|
+
|
|
40
40
|
// Testing frameworks
|
|
41
|
-
if (deps.includes('jest') || pkg.devDependencies?.jest) analysis.push('Testing: Jest')
|
|
42
|
-
if (deps.includes('vitest') || pkg.devDependencies?.vitest) analysis.push('Testing: Vitest')
|
|
43
|
-
if (deps.includes('mocha') || pkg.devDependencies?.mocha) analysis.push('Testing: Mocha')
|
|
41
|
+
if (deps.includes('jest') || pkg.devDependencies?.jest) analysis.push('Testing: Jest')
|
|
42
|
+
if (deps.includes('vitest') || pkg.devDependencies?.vitest) analysis.push('Testing: Vitest')
|
|
43
|
+
if (deps.includes('mocha') || pkg.devDependencies?.mocha) analysis.push('Testing: Mocha')
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
if (pkg.scripts) {
|
|
47
|
-
const scripts = Object.keys(pkg.scripts)
|
|
48
|
-
if (scripts.includes('build')) analysis.push('Build: Configured')
|
|
49
|
-
if (scripts.includes('test')) analysis.push('Test script: Available')
|
|
50
|
-
if (scripts.includes('lint')) analysis.push('Linting: Available')
|
|
51
|
-
if (scripts.includes('dev') || scripts.includes('start'))
|
|
47
|
+
const scripts = Object.keys(pkg.scripts)
|
|
48
|
+
if (scripts.includes('build')) analysis.push('Build: Configured')
|
|
49
|
+
if (scripts.includes('test')) analysis.push('Test script: Available')
|
|
50
|
+
if (scripts.includes('lint')) analysis.push('Linting: Available')
|
|
51
|
+
if (scripts.includes('dev') || scripts.includes('start'))
|
|
52
|
+
analysis.push('Dev server: Available')
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
|
-
|
|
55
|
+
|
|
55
56
|
// Directory structure analysis
|
|
56
|
-
const srcExists = existsSync(join(projectRoot, 'src'))
|
|
57
|
-
const componentsExists =
|
|
58
|
-
|
|
59
|
-
const libExists = existsSync(join(projectRoot, 'lib')) ||
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (
|
|
65
|
-
if (
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
|
|
57
|
+
const srcExists = existsSync(join(projectRoot, 'src'))
|
|
58
|
+
const componentsExists =
|
|
59
|
+
existsSync(join(projectRoot, 'src/components')) || existsSync(join(projectRoot, 'components'))
|
|
60
|
+
const libExists = existsSync(join(projectRoot, 'lib')) || existsSync(join(projectRoot, 'src/lib'))
|
|
61
|
+
const utilsExists =
|
|
62
|
+
existsSync(join(projectRoot, 'utils')) || existsSync(join(projectRoot, 'src/utils'))
|
|
63
|
+
|
|
64
|
+
if (srcExists) analysis.push('Structure: src/ directory')
|
|
65
|
+
if (componentsExists) analysis.push('Has: Components directory')
|
|
66
|
+
if (libExists) analysis.push('Has: Lib directory')
|
|
67
|
+
if (utilsExists) analysis.push('Has: Utils directory')
|
|
68
|
+
|
|
69
69
|
// Configuration files
|
|
70
70
|
const configs = [
|
|
71
71
|
{ file: 'tsconfig.json', desc: 'TypeScript config' },
|
|
@@ -76,78 +76,78 @@ async function analyzeCodebase(): Promise<string> {
|
|
|
76
76
|
{ file: 'next.config.js', desc: 'Next.js config' },
|
|
77
77
|
{ file: 'vite.config.ts', desc: 'Vite config' },
|
|
78
78
|
{ file: 'docker-compose.yml', desc: 'Docker Compose' },
|
|
79
|
-
{ file: 'Dockerfile', desc: 'Docker' }
|
|
80
|
-
]
|
|
81
|
-
|
|
79
|
+
{ file: 'Dockerfile', desc: 'Docker' },
|
|
80
|
+
]
|
|
81
|
+
|
|
82
82
|
for (const config of configs) {
|
|
83
83
|
if (existsSync(join(projectRoot, config.file))) {
|
|
84
|
-
analysis.push(`Config: ${config.desc}`)
|
|
84
|
+
analysis.push(`Config: ${config.desc}`)
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
-
|
|
87
|
+
|
|
88
88
|
// Check for specific project patterns
|
|
89
89
|
if (existsSync(join(projectRoot, '.plans'))) {
|
|
90
|
-
analysis.push('Project type: Uses hone for task management')
|
|
90
|
+
analysis.push('Project type: Uses hone for task management')
|
|
91
91
|
}
|
|
92
|
-
|
|
92
|
+
|
|
93
93
|
if (existsSync(join(projectRoot, 'AGENTS.md'))) {
|
|
94
|
-
analysis.push('Documentation: Has AGENTS.md (development guidelines)')
|
|
94
|
+
analysis.push('Documentation: Has AGENTS.md (development guidelines)')
|
|
95
95
|
}
|
|
96
|
-
|
|
96
|
+
|
|
97
97
|
// README analysis
|
|
98
98
|
if (existsSync(join(projectRoot, 'README.md'))) {
|
|
99
99
|
try {
|
|
100
|
-
const readme = await readFile(join(projectRoot, 'README.md'), 'utf-8')
|
|
101
|
-
const hasInstallSection = readme.toLowerCase().includes('install')
|
|
102
|
-
const hasUsageSection = readme.toLowerCase().includes('usage')
|
|
103
|
-
if (hasInstallSection) analysis.push('README: Has installation instructions')
|
|
104
|
-
if (hasUsageSection) analysis.push('README: Has usage instructions')
|
|
100
|
+
const readme = await readFile(join(projectRoot, 'README.md'), 'utf-8')
|
|
101
|
+
const hasInstallSection = readme.toLowerCase().includes('install')
|
|
102
|
+
const hasUsageSection = readme.toLowerCase().includes('usage')
|
|
103
|
+
if (hasInstallSection) analysis.push('README: Has installation instructions')
|
|
104
|
+
if (hasUsageSection) analysis.push('README: Has usage instructions')
|
|
105
105
|
} catch {
|
|
106
106
|
// Ignore errors reading README
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
-
|
|
109
|
+
|
|
110
110
|
// Check for common patterns in source files
|
|
111
111
|
if (srcExists) {
|
|
112
112
|
try {
|
|
113
|
-
const srcFiles = await readdir(join(projectRoot, 'src'))
|
|
113
|
+
const srcFiles = await readdir(join(projectRoot, 'src'))
|
|
114
114
|
if (srcFiles.includes('index.ts') || srcFiles.includes('index.js')) {
|
|
115
|
-
analysis.push('Entry point: src/index')
|
|
115
|
+
analysis.push('Entry point: src/index')
|
|
116
116
|
}
|
|
117
117
|
if (srcFiles.includes('cli.ts') || srcFiles.includes('cli.js')) {
|
|
118
|
-
analysis.push('Has: CLI module')
|
|
118
|
+
analysis.push('Has: CLI module')
|
|
119
119
|
}
|
|
120
120
|
if (srcFiles.includes('config.ts') || srcFiles.includes('config.js')) {
|
|
121
|
-
analysis.push('Has: Configuration module')
|
|
121
|
+
analysis.push('Has: Configuration module')
|
|
122
122
|
}
|
|
123
123
|
if (srcFiles.some(f => f.includes('test'))) {
|
|
124
|
-
analysis.push('Testing: Test files in src')
|
|
124
|
+
analysis.push('Testing: Test files in src')
|
|
125
125
|
}
|
|
126
126
|
} catch {
|
|
127
127
|
// Ignore errors reading src directory
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
|
-
|
|
131
|
-
return analysis.length > 0 ? analysis.join(', ') : 'No specific patterns detected'
|
|
130
|
+
|
|
131
|
+
return analysis.length > 0 ? analysis.join(', ') : 'No specific patterns detected'
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
interface QAResponse {
|
|
135
|
-
question: string | null
|
|
136
|
-
shouldContinue: boolean
|
|
135
|
+
question: string | null
|
|
136
|
+
shouldContinue: boolean
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
async function askQuestion(prompt: string): Promise<string> {
|
|
140
140
|
const rl = readline.createInterface({
|
|
141
141
|
input: process.stdin,
|
|
142
|
-
output: process.stdout
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
return new Promise(
|
|
146
|
-
rl.question(prompt,
|
|
147
|
-
rl.close()
|
|
148
|
-
resolve(answer.trim())
|
|
149
|
-
})
|
|
150
|
-
})
|
|
142
|
+
output: process.stdout,
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
return new Promise(resolve => {
|
|
146
|
+
rl.question(prompt, answer => {
|
|
147
|
+
rl.close()
|
|
148
|
+
resolve(answer.trim())
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
async function generateClarifyingQuestion(
|
|
@@ -156,37 +156,48 @@ async function generateClarifyingQuestion(
|
|
|
156
156
|
previousQA: Array<{ question: string; answer: string }>,
|
|
157
157
|
roundNumber: number
|
|
158
158
|
): Promise<QAResponse> {
|
|
159
|
-
const config = await loadConfig()
|
|
160
|
-
const model = resolveModelForPhase(config, 'prd')
|
|
161
|
-
|
|
162
|
-
const client = new AgentClient({
|
|
159
|
+
const config = await loadConfig()
|
|
160
|
+
const model = resolveModelForPhase(config, 'prd')
|
|
161
|
+
|
|
162
|
+
const client = new AgentClient({
|
|
163
163
|
agent: config.defaultAgent,
|
|
164
|
-
model
|
|
165
|
-
})
|
|
166
|
-
|
|
164
|
+
model,
|
|
165
|
+
})
|
|
166
|
+
|
|
167
167
|
// Show progress indicator for question generation
|
|
168
|
-
process.stdout.write(`Generating question ${roundNumber}... `)
|
|
169
|
-
|
|
170
|
-
const qaHistory = previousQA.map(qa => `Q: ${qa.question}\nA: ${qa.answer}`).join('\n\n')
|
|
171
|
-
|
|
168
|
+
process.stdout.write(`Generating question ${roundNumber}... `)
|
|
169
|
+
|
|
170
|
+
const qaHistory = previousQA.map(qa => `Q: ${qa.question}\nA: ${qa.answer}`).join('\n\n')
|
|
171
|
+
|
|
172
172
|
// Read AGENTS.md for context about the project
|
|
173
|
-
let agentsContent = ''
|
|
174
|
-
const agentsPath = join(process.cwd(), 'AGENTS.md')
|
|
173
|
+
let agentsContent = ''
|
|
174
|
+
const agentsPath = join(process.cwd(), 'AGENTS.md')
|
|
175
175
|
if (existsSync(agentsPath)) {
|
|
176
176
|
try {
|
|
177
|
-
agentsContent = await readFile(agentsPath, 'utf-8')
|
|
177
|
+
agentsContent = await readFile(agentsPath, 'utf-8')
|
|
178
178
|
} catch {
|
|
179
179
|
// Ignore errors reading AGENTS.md
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
|
-
|
|
182
|
+
|
|
183
183
|
const systemPrompt = `You are helping generate a Product Requirements Document (PRD) for a software feature.
|
|
184
184
|
The user has provided a feature description, and you need to ask clarifying questions to make the PRD comprehensive.
|
|
185
185
|
|
|
186
|
-
IMPORTANT: Before asking questions, carefully analyze the provided codebase context and project documentation.
|
|
187
|
-
Many common questions about testing frameworks, build systems, dependencies, project structure, and technical
|
|
186
|
+
IMPORTANT: Before asking questions, carefully analyze the provided codebase context and project documentation.
|
|
187
|
+
Many common questions about testing frameworks, build systems, dependencies, project structure, and technical
|
|
188
188
|
patterns can be answered from the codebase analysis. Only ask questions that cannot be determined from the code.
|
|
189
189
|
|
|
190
|
+
FILE AND URL PROCESSING:
|
|
191
|
+
- If the feature description contains file paths (e.g., "./file.txt", "/path/to/file", "src/component.js"), automatically read and incorporate their content using the Read tool
|
|
192
|
+
- If the feature description contains URLs (e.g., "https://example.com", "http://site.com/page"), automatically fetch and incorporate their content using the WebFetch tool
|
|
193
|
+
- Process these references transparently - no need to ask the user about them unless you cannot access them
|
|
194
|
+
- REFERENCE ACCESS FAILURES: When file reads or URL fetches fail:
|
|
195
|
+
* Continue with question generation using available context
|
|
196
|
+
* Ask clarifying questions about the inaccessible reference's intended purpose or relevance
|
|
197
|
+
* Examples: "I couldn't access [file/URL]. What was its relevance to this feature?" or "Since [reference] is inaccessible, could you clarify what information it contained that's relevant to the requirements?"
|
|
198
|
+
* Prioritize these clarification questions early in the Q&A session when references are critical to understanding scope
|
|
199
|
+
- Use the content from files and URLs to better inform your clarifying questions when accessible
|
|
200
|
+
|
|
190
201
|
Rules:
|
|
191
202
|
- Ask ONE specific, focused question at a time
|
|
192
203
|
- Questions should help clarify requirements, scope, UX, technical approach, or edge cases
|
|
@@ -197,38 +208,45 @@ Rules:
|
|
|
197
208
|
|
|
198
209
|
Codebase context: ${codebaseAnalysis}
|
|
199
210
|
|
|
200
|
-
${
|
|
211
|
+
${
|
|
212
|
+
agentsContent
|
|
213
|
+
? `Project documentation (AGENTS.md):
|
|
201
214
|
${agentsContent}
|
|
202
215
|
|
|
203
|
-
`
|
|
216
|
+
`
|
|
217
|
+
: ''
|
|
218
|
+
}Feature description: ${featureDescription}
|
|
204
219
|
|
|
205
|
-
${qaHistory ? `Previous Q&A:\n${qaHistory}` : 'This is the first question.'}
|
|
220
|
+
${qaHistory ? `Previous Q&A:\n${qaHistory}` : 'This is the first question.'}`
|
|
206
221
|
|
|
207
222
|
try {
|
|
208
223
|
const response = await client.messages.create({
|
|
209
224
|
max_tokens: 500,
|
|
210
|
-
messages: [
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
225
|
+
messages: [
|
|
226
|
+
{
|
|
227
|
+
role: 'user',
|
|
228
|
+
content:
|
|
229
|
+
'What is your next clarifying question, or respond with "DONE" if you have enough information?',
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
system: systemPrompt,
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
const content = response.content[0]
|
|
236
|
+
const text = content && content.type === 'text' ? content.text.trim() : ''
|
|
237
|
+
|
|
220
238
|
// Clear progress indicator
|
|
221
|
-
process.stdout.write('✓\n')
|
|
222
|
-
|
|
239
|
+
process.stdout.write('✓\n')
|
|
240
|
+
|
|
223
241
|
if (text.toUpperCase().includes('DONE') || text === '') {
|
|
224
|
-
return { question: null, shouldContinue: false }
|
|
242
|
+
return { question: null, shouldContinue: false }
|
|
225
243
|
}
|
|
226
|
-
|
|
227
|
-
return { question: text, shouldContinue: true }
|
|
244
|
+
|
|
245
|
+
return { question: text, shouldContinue: true }
|
|
228
246
|
} catch (error) {
|
|
229
247
|
// Clear progress indicator with error
|
|
230
|
-
process.stdout.write('✗\n')
|
|
231
|
-
throw error
|
|
248
|
+
process.stdout.write('✗\n')
|
|
249
|
+
throw error
|
|
232
250
|
}
|
|
233
251
|
}
|
|
234
252
|
|
|
@@ -237,32 +255,39 @@ async function generatePRDContent(
|
|
|
237
255
|
codebaseAnalysis: string,
|
|
238
256
|
qa: Array<{ question: string; answer: string }>
|
|
239
257
|
): Promise<{ content: string; featureName: string }> {
|
|
240
|
-
const config = await loadConfig()
|
|
241
|
-
const model = resolveModelForPhase(config, 'prd')
|
|
242
|
-
|
|
258
|
+
const config = await loadConfig()
|
|
259
|
+
const model = resolveModelForPhase(config, 'prd')
|
|
260
|
+
|
|
243
261
|
const client = new AgentClient({
|
|
244
262
|
agent: config.defaultAgent,
|
|
245
|
-
model
|
|
246
|
-
})
|
|
247
|
-
|
|
263
|
+
model,
|
|
264
|
+
})
|
|
265
|
+
|
|
248
266
|
// Show progress indicator for PRD generation
|
|
249
|
-
process.stdout.write('Writing PRD document... ')
|
|
250
|
-
|
|
251
|
-
const qaHistory = qa.map(q => `Q: ${q.question}\nA: ${q.answer}`).join('\n\n')
|
|
252
|
-
|
|
267
|
+
process.stdout.write('Writing PRD document... ')
|
|
268
|
+
|
|
269
|
+
const qaHistory = qa.map(q => `Q: ${q.question}\nA: ${q.answer}`).join('\n\n')
|
|
270
|
+
|
|
253
271
|
// Read AGENTS.md for context about the project
|
|
254
|
-
let agentsContent = ''
|
|
255
|
-
const agentsPath = join(process.cwd(), 'AGENTS.md')
|
|
272
|
+
let agentsContent = ''
|
|
273
|
+
const agentsPath = join(process.cwd(), 'AGENTS.md')
|
|
256
274
|
if (existsSync(agentsPath)) {
|
|
257
275
|
try {
|
|
258
|
-
agentsContent = await readFile(agentsPath, 'utf-8')
|
|
276
|
+
agentsContent = await readFile(agentsPath, 'utf-8')
|
|
259
277
|
} catch {
|
|
260
278
|
// Ignore errors reading AGENTS.md
|
|
261
279
|
}
|
|
262
280
|
}
|
|
263
|
-
|
|
281
|
+
|
|
264
282
|
const systemPrompt = `You are a technical product manager writing a Product Requirements Document (PRD).
|
|
265
283
|
|
|
284
|
+
FILE AND URL PROCESSING:
|
|
285
|
+
- If the feature description contains file paths (e.g., "./file.txt", "/path/to/file", "src/component.js"), automatically read and incorporate their content using the Read tool
|
|
286
|
+
- If the feature description contains URLs (e.g., "https://example.com", "http://site.com/page"), automatically fetch and incorporate their content using the WebFetch tool
|
|
287
|
+
- Process these references transparently to enrich the PRD content with actual file contents and web page information
|
|
288
|
+
- If references fail to load, note them in the "Open Questions" section and continue PRD generation
|
|
289
|
+
- Use the content from files and URLs to inform technical decisions, requirements, and acceptance criteria
|
|
290
|
+
|
|
266
291
|
Generate a comprehensive PRD with the following structure:
|
|
267
292
|
|
|
268
293
|
# PRD: <Feature Name>
|
|
@@ -303,53 +328,56 @@ Context:
|
|
|
303
328
|
- Feature: ${featureDescription}
|
|
304
329
|
${agentsContent ? `\n- Project Documentation (AGENTS.md):\n${agentsContent}` : ''}${qaHistory ? `\n- Q&A Session:\n${qaHistory}` : ''}
|
|
305
330
|
|
|
306
|
-
Write a complete, detailed PRD following the structure above. Use the codebase analysis and project
|
|
307
|
-
documentation to inform technical decisions and ensure the PRD aligns with the existing project patterns
|
|
331
|
+
Write a complete, detailed PRD following the structure above. Use the codebase analysis and project
|
|
332
|
+
documentation to inform technical decisions and ensure the PRD aligns with the existing project patterns.`
|
|
308
333
|
|
|
309
334
|
try {
|
|
310
335
|
const response = await client.messages.create({
|
|
311
336
|
max_tokens: 4000,
|
|
312
|
-
messages: [
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
337
|
+
messages: [
|
|
338
|
+
{
|
|
339
|
+
role: 'user',
|
|
340
|
+
content: 'Generate the PRD now.',
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
system: systemPrompt,
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
const content = response.content[0]
|
|
347
|
+
const prdContent = content && content.type === 'text' ? content.text : ''
|
|
348
|
+
|
|
322
349
|
if (!prdContent.trim()) {
|
|
323
|
-
throw new Error('Agent returned empty PRD content')
|
|
350
|
+
throw new Error('Agent returned empty PRD content')
|
|
324
351
|
}
|
|
325
|
-
|
|
352
|
+
|
|
326
353
|
// Extract feature name from the first heading
|
|
327
|
-
const match = prdContent.match(/# PRD: (.+)/)
|
|
328
|
-
const featureName = match && match[1] ? match[1].trim() : featureDescription
|
|
329
|
-
|
|
354
|
+
const match = prdContent.match(/# PRD: (.+)/)
|
|
355
|
+
const featureName = match && match[1] ? match[1].trim() : featureDescription
|
|
356
|
+
|
|
330
357
|
// Clear progress indicator
|
|
331
|
-
process.stdout.write('✓\n')
|
|
332
|
-
|
|
333
|
-
return { content: prdContent, featureName }
|
|
358
|
+
process.stdout.write('✓\n')
|
|
359
|
+
|
|
360
|
+
return { content: prdContent, featureName }
|
|
334
361
|
} catch (error) {
|
|
335
362
|
// Clear progress indicator with error
|
|
336
|
-
process.stdout.write('✗\n')
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
363
|
+
process.stdout.write('✗\n')
|
|
364
|
+
|
|
365
|
+
// Re-throw the error to be handled by the caller
|
|
366
|
+
// This allows the generatePRD function to implement fallback behavior
|
|
367
|
+
throw error
|
|
340
368
|
}
|
|
341
369
|
}
|
|
342
370
|
|
|
343
371
|
export async function generatePRD(featureDescription: string): Promise<string> {
|
|
344
|
-
console.log('\nAnalyzing codebase...')
|
|
345
|
-
const codebaseAnalysis = await analyzeCodebase()
|
|
346
|
-
console.log(`Found: ${codebaseAnalysis}\n`)
|
|
347
|
-
|
|
348
|
-
const qa: Array<{ question: string; answer: string }> = []
|
|
349
|
-
const maxRounds = 5
|
|
350
|
-
|
|
351
|
-
console.log('I have a few questions to refine this PRD:\n')
|
|
352
|
-
|
|
372
|
+
console.log('\nAnalyzing codebase...')
|
|
373
|
+
const codebaseAnalysis = await analyzeCodebase()
|
|
374
|
+
console.log(`Found: ${codebaseAnalysis}\n`)
|
|
375
|
+
|
|
376
|
+
const qa: Array<{ question: string; answer: string }> = []
|
|
377
|
+
const maxRounds = 5
|
|
378
|
+
|
|
379
|
+
console.log('I have a few questions to refine this PRD:\n')
|
|
380
|
+
|
|
353
381
|
for (let round = 1; round <= maxRounds; round++) {
|
|
354
382
|
try {
|
|
355
383
|
const { question, shouldContinue } = await generateClarifyingQuestion(
|
|
@@ -357,48 +385,106 @@ export async function generatePRD(featureDescription: string): Promise<string> {
|
|
|
357
385
|
codebaseAnalysis,
|
|
358
386
|
qa,
|
|
359
387
|
round
|
|
360
|
-
)
|
|
361
|
-
|
|
388
|
+
)
|
|
389
|
+
|
|
362
390
|
if (!shouldContinue || !question) {
|
|
363
|
-
break
|
|
391
|
+
break
|
|
364
392
|
}
|
|
365
|
-
|
|
366
|
-
console.log(`${round}. ${question}`)
|
|
367
|
-
const answer = await askQuestion('> ')
|
|
368
|
-
|
|
393
|
+
|
|
394
|
+
console.log(`${round}. ${question}`)
|
|
395
|
+
const answer = await askQuestion('> ')
|
|
396
|
+
|
|
369
397
|
if (answer.toLowerCase() === 'done') {
|
|
370
|
-
break
|
|
398
|
+
break
|
|
371
399
|
}
|
|
372
|
-
|
|
373
|
-
qa.push({ question, answer })
|
|
374
|
-
console.log('')
|
|
400
|
+
|
|
401
|
+
qa.push({ question, answer })
|
|
402
|
+
console.log('')
|
|
375
403
|
} catch (error) {
|
|
376
|
-
console.error(
|
|
377
|
-
|
|
378
|
-
|
|
404
|
+
console.error(
|
|
405
|
+
`\nError generating clarifying question for round ${round}: ${error instanceof Error ? error.message : String(error)}`
|
|
406
|
+
)
|
|
407
|
+
console.log('Continuing with available information...\n')
|
|
408
|
+
break
|
|
379
409
|
}
|
|
380
410
|
}
|
|
381
|
-
|
|
382
|
-
console.log('\nGenerating PRD...')
|
|
411
|
+
|
|
412
|
+
console.log('\nGenerating PRD...')
|
|
383
413
|
try {
|
|
384
414
|
const { content, featureName } = await generatePRDContent(
|
|
385
415
|
featureDescription,
|
|
386
416
|
codebaseAnalysis,
|
|
387
417
|
qa
|
|
388
|
-
)
|
|
389
|
-
|
|
390
|
-
const slug = slugify(featureName)
|
|
391
|
-
const filename = `prd-${slug}.md
|
|
392
|
-
const filepath = join(process.cwd(), '.plans', filename)
|
|
393
|
-
|
|
394
|
-
await writeFile(filepath, content, 'utf-8')
|
|
395
|
-
|
|
396
|
-
console.log(`✓ Saved to .plans/${filename}\n`)
|
|
397
|
-
|
|
398
|
-
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
const slug = slugify(featureName)
|
|
421
|
+
const filename = `prd-${slug}.md`
|
|
422
|
+
const filepath = join(process.cwd(), '.plans', filename)
|
|
423
|
+
|
|
424
|
+
await writeFile(filepath, content, 'utf-8')
|
|
425
|
+
|
|
426
|
+
console.log(`✓ Saved to .plans/${filename}\n`)
|
|
427
|
+
console.log(
|
|
428
|
+
`Review and edit if needed the PRD and then execute "hone prd-to-tasks .plans/${filename}"\n`
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
return filename
|
|
399
432
|
} catch (error) {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
433
|
+
console.error(
|
|
434
|
+
`\nError generating PRD: ${error instanceof Error ? error.message : String(error)}`
|
|
435
|
+
)
|
|
436
|
+
console.log('Generating fallback PRD with available information...\n')
|
|
437
|
+
|
|
438
|
+
// Generate a fallback PRD with basic structure and available context
|
|
439
|
+
const fallbackFeatureName = featureDescription.split('\n')[0] || 'New Feature'
|
|
440
|
+
const slug = slugify(fallbackFeatureName)
|
|
441
|
+
const filename = `prd-${slug}.md`
|
|
442
|
+
const filepath = join(process.cwd(), '.plans', filename)
|
|
443
|
+
|
|
444
|
+
const qaHistory = qa.map(q => `Q: ${q.question}\nA: ${q.answer}`).join('\n\n')
|
|
445
|
+
|
|
446
|
+
const fallbackContent = `# PRD: ${fallbackFeatureName}
|
|
447
|
+
|
|
448
|
+
## Overview
|
|
449
|
+
${featureDescription}
|
|
450
|
+
|
|
451
|
+
## Goals
|
|
452
|
+
To be determined based on further analysis.
|
|
453
|
+
|
|
454
|
+
## Non-Goals
|
|
455
|
+
To be determined based on further analysis.
|
|
456
|
+
|
|
457
|
+
## Requirements
|
|
458
|
+
|
|
459
|
+
### Functional Requirements
|
|
460
|
+
- REQ-F-001: To be defined based on detailed analysis
|
|
461
|
+
|
|
462
|
+
### Non-Functional Requirements
|
|
463
|
+
- REQ-NF-001: To be defined based on detailed analysis
|
|
464
|
+
|
|
465
|
+
## Technical Considerations
|
|
466
|
+
Based on codebase analysis: ${codebaseAnalysis}
|
|
467
|
+
|
|
468
|
+
## Acceptance Criteria
|
|
469
|
+
- [ ] Requirements to be defined after successful agent processing
|
|
470
|
+
|
|
471
|
+
## Out of Scope
|
|
472
|
+
To be determined.
|
|
473
|
+
|
|
474
|
+
## Open Questions
|
|
475
|
+
- This PRD was generated in fallback mode due to agent processing issues
|
|
476
|
+
- Full requirements analysis could not be completed
|
|
477
|
+
- Consider regenerating this PRD when system connectivity is restored
|
|
478
|
+
${qaHistory ? `\n\nPrevious Q&A Session:\n${qaHistory}` : ''}`
|
|
479
|
+
|
|
480
|
+
await writeFile(filepath, fallbackContent, 'utf-8')
|
|
481
|
+
|
|
482
|
+
console.log(`✓ Generated fallback PRD: .plans/${filename}`)
|
|
483
|
+
console.log(
|
|
484
|
+
'Note: This PRD contains basic structure. Consider regenerating when system is available.\n'
|
|
485
|
+
)
|
|
486
|
+
console.log(`Review and edit the PRD and then execute "hone prd-to-tasks .plans/${filename}"\n`)
|
|
487
|
+
|
|
488
|
+
return filename
|
|
403
489
|
}
|
|
404
490
|
}
|