hone-ai 0.5.0 → 0.10.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 +5 -2
- 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/status.ts
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
import { readdirSync, existsSync } from 'fs'
|
|
2
|
-
import { join } from 'path'
|
|
3
|
-
import { getPlansDir } from './config'
|
|
4
|
-
import { loadTaskFile, calculateStatus } from './prds'
|
|
5
|
-
import type { Task, TaskFile } from './prds'
|
|
1
|
+
import { readdirSync, existsSync } from 'fs'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import { getPlansDir } from './config'
|
|
4
|
+
import { loadTaskFile, calculateStatus } from './prds'
|
|
5
|
+
import type { Task, TaskFile } from './prds'
|
|
6
6
|
|
|
7
7
|
export interface TaskFileStatus {
|
|
8
|
-
filename: string
|
|
9
|
-
feature: string
|
|
10
|
-
completedCount: number
|
|
11
|
-
totalCount: number
|
|
12
|
-
nextTask: Task | null
|
|
8
|
+
filename: string
|
|
9
|
+
feature: string
|
|
10
|
+
completedCount: number
|
|
11
|
+
totalCount: number
|
|
12
|
+
nextTask: Task | null
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Get all task files in .plans/ directory
|
|
17
17
|
*/
|
|
18
18
|
export function listTaskFiles(): string[] {
|
|
19
|
-
const plansDir = getPlansDir()
|
|
19
|
+
const plansDir = getPlansDir()
|
|
20
20
|
if (!existsSync(plansDir)) {
|
|
21
|
-
return []
|
|
21
|
+
return []
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
const files = readdirSync(plansDir)
|
|
25
|
-
return files.filter(file => file.startsWith('tasks-') && file.endsWith('.yml'))
|
|
23
|
+
|
|
24
|
+
const files = readdirSync(plansDir)
|
|
25
|
+
return files.filter(file => file.startsWith('tasks-') && file.endsWith('.yml'))
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/**
|
|
@@ -31,64 +31,62 @@ export function listTaskFiles(): string[] {
|
|
|
31
31
|
*/
|
|
32
32
|
export function findNextTask(taskFile: TaskFile): Task | null {
|
|
33
33
|
if (!taskFile.tasks || taskFile.tasks.length === 0) {
|
|
34
|
-
return null
|
|
34
|
+
return null
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
// Find first pending task where all dependencies are satisfied
|
|
38
38
|
for (const task of taskFile.tasks) {
|
|
39
39
|
if (task.status === 'pending') {
|
|
40
40
|
// Check if all dependencies are completed or cancelled
|
|
41
|
-
const dependencies = task.dependencies || []
|
|
41
|
+
const dependencies = task.dependencies || []
|
|
42
42
|
const allDepsCompleted = dependencies.every(depId => {
|
|
43
|
-
const depTask = taskFile.tasks.find(t => t.id === depId)
|
|
44
|
-
return depTask && (depTask.status === 'completed' || depTask.status === 'cancelled')
|
|
45
|
-
})
|
|
46
|
-
|
|
43
|
+
const depTask = taskFile.tasks.find(t => t.id === depId)
|
|
44
|
+
return depTask && (depTask.status === 'completed' || depTask.status === 'cancelled')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
47
|
if (allDepsCompleted) {
|
|
48
|
-
return task
|
|
48
|
+
return task
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
return null
|
|
52
|
+
|
|
53
|
+
return null
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
57
|
* Get status for a single task file
|
|
58
58
|
*/
|
|
59
59
|
export async function getTaskFileStatus(taskFilename: string): Promise<TaskFileStatus | null> {
|
|
60
|
-
const taskFile = await loadTaskFile(taskFilename)
|
|
60
|
+
const taskFile = await loadTaskFile(taskFilename)
|
|
61
61
|
if (!taskFile) {
|
|
62
|
-
return null
|
|
62
|
+
return null
|
|
63
63
|
}
|
|
64
|
-
|
|
65
|
-
const { status, completedCount, totalCount } = calculateStatus(taskFile)
|
|
66
|
-
|
|
64
|
+
|
|
65
|
+
const { status, completedCount, totalCount } = calculateStatus(taskFile)
|
|
66
|
+
|
|
67
67
|
// Skip fully completed files
|
|
68
68
|
if (status === 'completed') {
|
|
69
|
-
return null
|
|
69
|
+
return null
|
|
70
70
|
}
|
|
71
|
-
|
|
72
|
-
const nextTask = findNextTask(taskFile)
|
|
73
|
-
|
|
71
|
+
|
|
72
|
+
const nextTask = findNextTask(taskFile)
|
|
73
|
+
|
|
74
74
|
return {
|
|
75
75
|
filename: taskFilename,
|
|
76
76
|
feature: taskFile.feature,
|
|
77
77
|
completedCount,
|
|
78
78
|
totalCount,
|
|
79
|
-
nextTask
|
|
80
|
-
}
|
|
79
|
+
nextTask,
|
|
80
|
+
}
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
/**
|
|
84
84
|
* List all incomplete task files with their status
|
|
85
85
|
*/
|
|
86
86
|
export async function listIncompleteTaskFiles(): Promise<TaskFileStatus[]> {
|
|
87
|
-
const taskFiles = listTaskFiles()
|
|
88
|
-
const statusList = await Promise.all(
|
|
89
|
-
|
|
90
|
-
);
|
|
91
|
-
|
|
87
|
+
const taskFiles = listTaskFiles()
|
|
88
|
+
const statusList = await Promise.all(taskFiles.map(file => getTaskFileStatus(file)))
|
|
89
|
+
|
|
92
90
|
// Filter out null (completed files) and return
|
|
93
|
-
return statusList.filter((s): s is TaskFileStatus => s !== null)
|
|
91
|
+
return statusList.filter((s): s is TaskFileStatus => s !== null)
|
|
94
92
|
}
|
|
@@ -1,64 +1,96 @@
|
|
|
1
|
-
import { describe, test, expect, beforeEach, afterEach, beforeAll, afterAll } from 'bun:test'
|
|
2
|
-
import { generateTasksFromPRD } from './task-generator'
|
|
3
|
-
import { writeFile, mkdir, rm } from 'fs/promises'
|
|
4
|
-
import { join } from 'path'
|
|
5
|
-
import { existsSync } from 'fs'
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach, beforeAll, afterAll, mock } from 'bun:test'
|
|
2
|
+
import { generateTasksFromPRD } from './task-generator'
|
|
3
|
+
import { writeFile, mkdir, rm, readFile } from 'fs/promises'
|
|
4
|
+
import { join } from 'path'
|
|
5
|
+
import { existsSync } from 'fs'
|
|
6
6
|
|
|
7
7
|
// Set test environment
|
|
8
|
-
const originalEnv = process.env.BUN_ENV
|
|
8
|
+
const originalEnv = process.env.BUN_ENV
|
|
9
9
|
beforeAll(() => {
|
|
10
|
-
process.env.BUN_ENV = 'test'
|
|
11
|
-
})
|
|
10
|
+
process.env.BUN_ENV = 'test'
|
|
11
|
+
})
|
|
12
12
|
afterAll(() => {
|
|
13
|
-
process.env.BUN_ENV = originalEnv
|
|
14
|
-
})
|
|
13
|
+
process.env.BUN_ENV = originalEnv
|
|
14
|
+
})
|
|
15
15
|
|
|
16
|
-
const TEST_WORKSPACE = join(process.cwd(), '.test-task-generator')
|
|
17
|
-
const TEST_PLANS_DIR = join(TEST_WORKSPACE, '.plans')
|
|
16
|
+
const TEST_WORKSPACE = join(process.cwd(), '.test-task-generator')
|
|
17
|
+
const TEST_PLANS_DIR = join(TEST_WORKSPACE, '.plans')
|
|
18
18
|
|
|
19
19
|
describe('task-generator', () => {
|
|
20
20
|
beforeEach(async () => {
|
|
21
|
+
// Mock AgentClient to avoid real API calls
|
|
22
|
+
mock.module('./agent-client', () => ({
|
|
23
|
+
AgentClient: function () {
|
|
24
|
+
return {
|
|
25
|
+
messages: {
|
|
26
|
+
create: mock().mockResolvedValue({
|
|
27
|
+
content: [
|
|
28
|
+
{
|
|
29
|
+
type: 'text',
|
|
30
|
+
text: JSON.stringify([
|
|
31
|
+
{
|
|
32
|
+
id: 'task-001',
|
|
33
|
+
title: 'Test task',
|
|
34
|
+
description: 'Test task description',
|
|
35
|
+
status: 'pending',
|
|
36
|
+
dependencies: [],
|
|
37
|
+
acceptance_criteria: ['Task works'],
|
|
38
|
+
completed_at: null,
|
|
39
|
+
},
|
|
40
|
+
]),
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
}),
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
}))
|
|
48
|
+
|
|
49
|
+
// Increase timeout to handle directory operations
|
|
21
50
|
// Create test workspace
|
|
22
51
|
if (existsSync(TEST_WORKSPACE)) {
|
|
23
|
-
await rm(TEST_WORKSPACE, { recursive: true, force: true })
|
|
52
|
+
await rm(TEST_WORKSPACE, { recursive: true, force: true })
|
|
24
53
|
}
|
|
25
|
-
await mkdir(TEST_WORKSPACE, { recursive: true })
|
|
26
|
-
await mkdir(TEST_PLANS_DIR, { recursive: true })
|
|
27
|
-
|
|
54
|
+
await mkdir(TEST_WORKSPACE, { recursive: true })
|
|
55
|
+
await mkdir(TEST_PLANS_DIR, { recursive: true })
|
|
56
|
+
|
|
28
57
|
// Change to test workspace
|
|
29
|
-
process.chdir(TEST_WORKSPACE)
|
|
30
|
-
})
|
|
31
|
-
|
|
58
|
+
process.chdir(TEST_WORKSPACE)
|
|
59
|
+
})
|
|
60
|
+
|
|
32
61
|
afterEach(async () => {
|
|
62
|
+
// Restore mocks
|
|
63
|
+
mock.restore()
|
|
64
|
+
|
|
33
65
|
// Restore original directory and cleanup
|
|
34
|
-
process.chdir(join(TEST_WORKSPACE, '..'))
|
|
66
|
+
process.chdir(join(TEST_WORKSPACE, '..'))
|
|
35
67
|
if (existsSync(TEST_WORKSPACE)) {
|
|
36
|
-
await rm(TEST_WORKSPACE, { recursive: true, force: true })
|
|
68
|
+
await rm(TEST_WORKSPACE, { recursive: true, force: true })
|
|
37
69
|
}
|
|
38
|
-
})
|
|
39
|
-
|
|
70
|
+
})
|
|
71
|
+
|
|
40
72
|
test('throws error if PRD file does not exist', async () => {
|
|
41
|
-
const nonExistentPath = join(TEST_PLANS_DIR, 'prd-nonexistent.md')
|
|
42
|
-
|
|
43
|
-
await expect(generateTasksFromPRD(nonExistentPath)).rejects.toThrow('File not found')
|
|
44
|
-
})
|
|
45
|
-
|
|
73
|
+
const nonExistentPath = join(TEST_PLANS_DIR, 'prd-nonexistent.md')
|
|
74
|
+
|
|
75
|
+
await expect(generateTasksFromPRD(nonExistentPath)).rejects.toThrow('File not found')
|
|
76
|
+
})
|
|
77
|
+
|
|
46
78
|
test('throws error if PRD filename format is invalid', async () => {
|
|
47
|
-
const invalidPath = join(TEST_PLANS_DIR, 'invalid-filename.md')
|
|
48
|
-
await writeFile(invalidPath, '# Test PRD', 'utf-8')
|
|
49
|
-
|
|
50
|
-
await expect(generateTasksFromPRD(invalidPath)).rejects.toThrow('Invalid PRD filename format')
|
|
51
|
-
})
|
|
52
|
-
|
|
79
|
+
const invalidPath = join(TEST_PLANS_DIR, 'invalid-filename.md')
|
|
80
|
+
await writeFile(invalidPath, '# Test PRD', 'utf-8')
|
|
81
|
+
|
|
82
|
+
await expect(generateTasksFromPRD(invalidPath)).rejects.toThrow('Invalid PRD filename format')
|
|
83
|
+
})
|
|
84
|
+
|
|
53
85
|
test('throws error if PRD filename has no feature name', async () => {
|
|
54
|
-
const invalidPath = join(TEST_PLANS_DIR, 'prd-.md')
|
|
55
|
-
await writeFile(invalidPath, '# Test PRD', 'utf-8')
|
|
56
|
-
|
|
57
|
-
await expect(generateTasksFromPRD(invalidPath)).rejects.toThrow('Invalid PRD filename format')
|
|
58
|
-
})
|
|
59
|
-
|
|
86
|
+
const invalidPath = join(TEST_PLANS_DIR, 'prd-.md')
|
|
87
|
+
await writeFile(invalidPath, '# Test PRD', 'utf-8')
|
|
88
|
+
|
|
89
|
+
await expect(generateTasksFromPRD(invalidPath)).rejects.toThrow('Invalid PRD filename format')
|
|
90
|
+
})
|
|
91
|
+
|
|
60
92
|
test('extracts feature name correctly from PRD filename', async () => {
|
|
61
|
-
const prdPath = join(TEST_PLANS_DIR, 'prd-test-feature.md')
|
|
93
|
+
const prdPath = join(TEST_PLANS_DIR, 'prd-test-feature.md')
|
|
62
94
|
const prdContent = `# PRD: Test Feature
|
|
63
95
|
|
|
64
96
|
## Overview
|
|
@@ -66,28 +98,23 @@ Simple test feature for unit testing.
|
|
|
66
98
|
|
|
67
99
|
## Requirements
|
|
68
100
|
- REQ-1: Basic requirement
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
await writeFile(prdPath, prdContent, 'utf-8')
|
|
72
|
-
|
|
73
|
-
//
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
delete process.env.ANTHROPIC_API_KEY;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
});
|
|
101
|
+
`
|
|
102
|
+
|
|
103
|
+
await writeFile(prdPath, prdContent, 'utf-8')
|
|
104
|
+
|
|
105
|
+
// With AgentClient mocked, this should succeed and create the tasks file
|
|
106
|
+
const result = await generateTasksFromPRD(prdPath)
|
|
107
|
+
|
|
108
|
+
// Verify the feature name was correctly extracted from the filename
|
|
109
|
+
expect(result).toBe('tasks-test-feature.yml')
|
|
110
|
+
|
|
111
|
+
// Verify the tasks file was created with correct content
|
|
112
|
+
const tasksFilePath = join(process.cwd(), '.plans', 'tasks-test-feature.yml')
|
|
113
|
+
expect(existsSync(tasksFilePath)).toBe(true)
|
|
114
|
+
|
|
115
|
+
// Read and verify the content contains the correct feature name
|
|
116
|
+
const tasksContent = await readFile(tasksFilePath, 'utf-8')
|
|
117
|
+
expect(tasksContent).toContain('feature: test-feature')
|
|
118
|
+
expect(tasksContent).toContain('prd: ./prd-test-feature.md')
|
|
119
|
+
})
|
|
120
|
+
})
|
package/src/task-generator.ts
CHANGED
|
@@ -1,84 +1,87 @@
|
|
|
1
|
-
import { loadConfig, resolveModelForPhase } from './config'
|
|
2
|
-
import { readFile, writeFile } from 'fs/promises'
|
|
3
|
-
import { join, basename } from 'path'
|
|
4
|
-
import { existsSync } from 'fs'
|
|
5
|
-
import { exitWithError, ErrorMessages } from './errors'
|
|
6
|
-
import { AgentClient } from './agent-client'
|
|
1
|
+
import { loadConfig, resolveModelForPhase } from './config'
|
|
2
|
+
import { readFile, writeFile } from 'fs/promises'
|
|
3
|
+
import { join, basename } from 'path'
|
|
4
|
+
import { existsSync } from 'fs'
|
|
5
|
+
import { exitWithError, ErrorMessages } from './errors'
|
|
6
|
+
import { AgentClient } from './agent-client'
|
|
7
7
|
|
|
8
8
|
interface Task {
|
|
9
|
-
id: string
|
|
10
|
-
title: string
|
|
11
|
-
description: string
|
|
12
|
-
status: 'pending' | 'in_progress' | 'completed'
|
|
13
|
-
dependencies: string[]
|
|
14
|
-
acceptance_criteria: string[]
|
|
15
|
-
completed_at: string | null
|
|
9
|
+
id: string
|
|
10
|
+
title: string
|
|
11
|
+
description: string
|
|
12
|
+
status: 'pending' | 'in_progress' | 'completed'
|
|
13
|
+
dependencies: string[]
|
|
14
|
+
acceptance_criteria: string[]
|
|
15
|
+
completed_at: string | null
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
interface TasksFile {
|
|
19
|
-
feature: string
|
|
20
|
-
prd: string
|
|
21
|
-
created_at: string
|
|
22
|
-
updated_at: string
|
|
23
|
-
tasks: Task[]
|
|
19
|
+
feature: string
|
|
20
|
+
prd: string
|
|
21
|
+
created_at: string
|
|
22
|
+
updated_at: string
|
|
23
|
+
tasks: Task[]
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export async function generateTasksFromPRD(prdFilePath: string): Promise<string> {
|
|
27
27
|
// Validate PRD file exists
|
|
28
28
|
if (!existsSync(prdFilePath)) {
|
|
29
|
-
const { message, details } = ErrorMessages.FILE_NOT_FOUND(prdFilePath)
|
|
30
|
-
exitWithError(message, details)
|
|
29
|
+
const { message, details } = ErrorMessages.FILE_NOT_FOUND(prdFilePath)
|
|
30
|
+
exitWithError(message, details)
|
|
31
31
|
}
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
// Read PRD content
|
|
34
|
-
const prdContent = await readFile(prdFilePath, 'utf-8')
|
|
35
|
-
|
|
34
|
+
const prdContent = await readFile(prdFilePath, 'utf-8')
|
|
35
|
+
|
|
36
36
|
// Extract feature name from PRD filename (prd-feature-name.md -> feature-name)
|
|
37
|
-
const prdFilename = basename(prdFilePath)
|
|
38
|
-
const featureMatch = prdFilename.match(/^prd-(.+)\.md$/)
|
|
37
|
+
const prdFilename = basename(prdFilePath)
|
|
38
|
+
const featureMatch = prdFilename.match(/^prd-(.+)\.md$/)
|
|
39
39
|
if (!featureMatch || !featureMatch[1]) {
|
|
40
|
-
throw new Error(`Invalid PRD filename format: ${prdFilename}. Expected: prd-<feature-name>.md`)
|
|
40
|
+
throw new Error(`Invalid PRD filename format: ${prdFilename}. Expected: prd-<feature-name>.md`)
|
|
41
41
|
}
|
|
42
|
-
const featureName = featureMatch[1]
|
|
43
|
-
|
|
44
|
-
console.log('\nAnalyzing PRD and generating tasks...')
|
|
45
|
-
|
|
46
|
-
const tasks = await generateTasksWithAI(prdContent)
|
|
47
|
-
|
|
42
|
+
const featureName = featureMatch[1]
|
|
43
|
+
|
|
44
|
+
console.log('\nAnalyzing PRD and generating tasks...')
|
|
45
|
+
|
|
46
|
+
const tasks = await generateTasksWithAI(prdContent)
|
|
47
|
+
|
|
48
48
|
// Create tasks file
|
|
49
|
-
const now = new Date().toISOString()
|
|
49
|
+
const now = new Date().toISOString()
|
|
50
50
|
const tasksFile: TasksFile = {
|
|
51
51
|
feature: featureName,
|
|
52
52
|
prd: `./${prdFilename}`,
|
|
53
53
|
created_at: now,
|
|
54
54
|
updated_at: now,
|
|
55
|
-
tasks
|
|
56
|
-
}
|
|
57
|
-
|
|
55
|
+
tasks,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
58
|
// Convert to YAML format
|
|
59
|
-
const yamlContent = formatAsYAML(tasksFile)
|
|
60
|
-
|
|
59
|
+
const yamlContent = formatAsYAML(tasksFile)
|
|
60
|
+
|
|
61
61
|
// Save to .plans/tasks-<feature-name>.yml
|
|
62
|
-
const tasksFilename = `tasks-${featureName}.yml
|
|
63
|
-
const tasksFilePath = join(process.cwd(), '.plans', tasksFilename)
|
|
64
|
-
|
|
65
|
-
await writeFile(tasksFilePath, yamlContent, 'utf-8')
|
|
66
|
-
|
|
67
|
-
console.log(`✓ Generated ${tasks.length} tasks`)
|
|
68
|
-
console.log(`✓ Saved to .plans/${tasksFilename}\n`)
|
|
69
|
-
|
|
70
|
-
|
|
62
|
+
const tasksFilename = `tasks-${featureName}.yml`
|
|
63
|
+
const tasksFilePath = join(process.cwd(), '.plans', tasksFilename)
|
|
64
|
+
|
|
65
|
+
await writeFile(tasksFilePath, yamlContent, 'utf-8')
|
|
66
|
+
|
|
67
|
+
console.log(`✓ Generated ${tasks.length} tasks`)
|
|
68
|
+
console.log(`✓ Saved to .plans/${tasksFilename}\n`)
|
|
69
|
+
console.log(
|
|
70
|
+
`Now run "hone run .plans/${tasksFilename} -i ${tasks.length}" to execute the tasks\n`
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return tasksFilename
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
async function generateTasksWithAI(prdContent: string): Promise<Task[]> {
|
|
74
|
-
const config = await loadConfig()
|
|
75
|
-
const model = resolveModelForPhase(config, 'prdToTasks')
|
|
76
|
-
|
|
77
|
+
const config = await loadConfig()
|
|
78
|
+
const model = resolveModelForPhase(config, 'prdToTasks')
|
|
79
|
+
|
|
77
80
|
const client = new AgentClient({
|
|
78
81
|
agent: config.defaultAgent,
|
|
79
|
-
model
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
+
model,
|
|
83
|
+
})
|
|
84
|
+
|
|
82
85
|
const systemPrompt = `You are a technical project manager breaking down a PRD into implementable tasks.
|
|
83
86
|
|
|
84
87
|
Generate an ordered list of tasks following these guidelines:
|
|
@@ -99,7 +102,7 @@ Generate an ordered list of tasks following these guidelines:
|
|
|
99
102
|
- Standard features
|
|
100
103
|
- Polish and refinements
|
|
101
104
|
|
|
102
|
-
3. **Dependencies**:
|
|
105
|
+
3. **Dependencies**:
|
|
103
106
|
- Identify which tasks must complete before others
|
|
104
107
|
- Use task IDs in dependencies array
|
|
105
108
|
- Keep dependency chains reasonable (don't over-constrain)
|
|
@@ -122,7 +125,7 @@ Example output:
|
|
|
122
125
|
"completed_at": null
|
|
123
126
|
},
|
|
124
127
|
{
|
|
125
|
-
"id": "task-002",
|
|
128
|
+
"id": "task-002",
|
|
126
129
|
"title": "Implement core API client",
|
|
127
130
|
"description": "Create API client with authentication, error handling, and retry logic. Should support all required endpoints.",
|
|
128
131
|
"status": "pending",
|
|
@@ -136,98 +139,108 @@ Example output:
|
|
|
136
139
|
}
|
|
137
140
|
]
|
|
138
141
|
|
|
139
|
-
Now analyze this PRD and generate tasks
|
|
142
|
+
Now analyze this PRD and generate tasks:`
|
|
140
143
|
|
|
141
144
|
try {
|
|
142
145
|
const response = await client.messages.create({
|
|
143
146
|
max_tokens: 8000,
|
|
144
|
-
messages: [
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
147
|
+
messages: [
|
|
148
|
+
{
|
|
149
|
+
role: 'user',
|
|
150
|
+
content: prdContent,
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
system: systemPrompt,
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
const content = response.content[0]
|
|
152
157
|
if (!content || content.type !== 'text') {
|
|
153
|
-
throw new Error('Invalid response from AI')
|
|
158
|
+
throw new Error('Invalid response from AI')
|
|
154
159
|
}
|
|
155
|
-
|
|
160
|
+
|
|
156
161
|
// Extract JSON array from response (handle cases where AI wraps in markdown code blocks)
|
|
157
|
-
let jsonText = content.text.trim()
|
|
158
|
-
const jsonMatch = jsonText.match(/```(?:json)?\s*(\[[\s\S]*\])\s*```/)
|
|
162
|
+
let jsonText = content.text.trim()
|
|
163
|
+
const jsonMatch = jsonText.match(/```(?:json)?\s*(\[[\s\S]*\])\s*```/)
|
|
159
164
|
if (jsonMatch && jsonMatch[1]) {
|
|
160
|
-
jsonText = jsonMatch[1]
|
|
165
|
+
jsonText = jsonMatch[1]
|
|
161
166
|
}
|
|
162
|
-
|
|
167
|
+
|
|
163
168
|
try {
|
|
164
|
-
const tasks = JSON.parse(jsonText)
|
|
165
|
-
|
|
169
|
+
const tasks = JSON.parse(jsonText)
|
|
170
|
+
|
|
166
171
|
if (!Array.isArray(tasks)) {
|
|
167
|
-
throw new Error('Response is not an array')
|
|
172
|
+
throw new Error('Response is not an array')
|
|
168
173
|
}
|
|
169
|
-
|
|
174
|
+
|
|
170
175
|
// Validate task structure
|
|
171
176
|
for (const task of tasks) {
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
|
|
177
|
+
if (
|
|
178
|
+
!task.id ||
|
|
179
|
+
!task.title ||
|
|
180
|
+
!task.description ||
|
|
181
|
+
!task.status ||
|
|
182
|
+
!Array.isArray(task.dependencies) ||
|
|
183
|
+
!Array.isArray(task.acceptance_criteria)
|
|
184
|
+
) {
|
|
185
|
+
throw new Error(`Invalid task structure: ${JSON.stringify(task)}`)
|
|
175
186
|
}
|
|
176
187
|
}
|
|
177
|
-
|
|
178
|
-
return tasks
|
|
188
|
+
|
|
189
|
+
return tasks
|
|
179
190
|
} catch (error) {
|
|
180
|
-
throw new Error(
|
|
191
|
+
throw new Error(
|
|
192
|
+
`Failed to parse AI response as JSON: ${error instanceof Error ? error.message : error}`
|
|
193
|
+
)
|
|
181
194
|
}
|
|
182
195
|
} catch (error) {
|
|
183
|
-
const { message, details } = ErrorMessages.NETWORK_ERROR_FINAL(error)
|
|
184
|
-
exitWithError(message, details)
|
|
185
|
-
throw error
|
|
196
|
+
const { message, details } = ErrorMessages.NETWORK_ERROR_FINAL(error)
|
|
197
|
+
exitWithError(message, details)
|
|
198
|
+
throw error // Never reached but satisfies TypeScript
|
|
186
199
|
}
|
|
187
200
|
}
|
|
188
201
|
|
|
189
202
|
function formatAsYAML(tasksFile: TasksFile): string {
|
|
190
|
-
const lines: string[] = []
|
|
191
|
-
|
|
192
|
-
lines.push(`feature: ${tasksFile.feature}`)
|
|
193
|
-
lines.push(`prd: ${tasksFile.prd}`)
|
|
194
|
-
lines.push(`created_at: ${tasksFile.created_at}`)
|
|
195
|
-
lines.push(`updated_at: ${tasksFile.updated_at}`)
|
|
196
|
-
lines.push('')
|
|
197
|
-
lines.push('tasks:')
|
|
198
|
-
|
|
203
|
+
const lines: string[] = []
|
|
204
|
+
|
|
205
|
+
lines.push(`feature: ${tasksFile.feature}`)
|
|
206
|
+
lines.push(`prd: ${tasksFile.prd}`)
|
|
207
|
+
lines.push(`created_at: ${tasksFile.created_at}`)
|
|
208
|
+
lines.push(`updated_at: ${tasksFile.updated_at}`)
|
|
209
|
+
lines.push('')
|
|
210
|
+
lines.push('tasks:')
|
|
211
|
+
|
|
199
212
|
for (const task of tasksFile.tasks) {
|
|
200
|
-
lines.push(` - id: ${task.id}`)
|
|
201
|
-
lines.push(` title: "${task.title}"`)
|
|
202
|
-
|
|
213
|
+
lines.push(` - id: ${task.id}`)
|
|
214
|
+
lines.push(` title: "${task.title}"`)
|
|
215
|
+
|
|
203
216
|
// Multi-line description with proper YAML indentation
|
|
204
|
-
lines.push(` description: |`)
|
|
205
|
-
const descLines = task.description.split('\n')
|
|
217
|
+
lines.push(` description: |`)
|
|
218
|
+
const descLines = task.description.split('\n')
|
|
206
219
|
for (const line of descLines) {
|
|
207
|
-
lines.push(` ${line}`)
|
|
220
|
+
lines.push(` ${line}`)
|
|
208
221
|
}
|
|
209
|
-
|
|
210
|
-
lines.push(` status: ${task.status}`)
|
|
211
|
-
|
|
222
|
+
|
|
223
|
+
lines.push(` status: ${task.status}`)
|
|
224
|
+
|
|
212
225
|
// Dependencies
|
|
213
226
|
if (task.dependencies.length === 0) {
|
|
214
|
-
lines.push(` dependencies: []`)
|
|
227
|
+
lines.push(` dependencies: []`)
|
|
215
228
|
} else {
|
|
216
|
-
lines.push(` dependencies:`)
|
|
229
|
+
lines.push(` dependencies:`)
|
|
217
230
|
for (const dep of task.dependencies) {
|
|
218
|
-
lines.push(` - ${dep}`)
|
|
231
|
+
lines.push(` - ${dep}`)
|
|
219
232
|
}
|
|
220
233
|
}
|
|
221
|
-
|
|
234
|
+
|
|
222
235
|
// Acceptance criteria
|
|
223
|
-
lines.push(` acceptance_criteria:`)
|
|
236
|
+
lines.push(` acceptance_criteria:`)
|
|
224
237
|
for (const criterion of task.acceptance_criteria) {
|
|
225
|
-
lines.push(` - "${criterion}"`)
|
|
238
|
+
lines.push(` - "${criterion}"`)
|
|
226
239
|
}
|
|
227
|
-
|
|
228
|
-
lines.push(` completed_at: ${task.completed_at || 'null'}`)
|
|
229
|
-
lines.push('')
|
|
240
|
+
|
|
241
|
+
lines.push(` completed_at: ${task.completed_at || 'null'}`)
|
|
242
|
+
lines.push('')
|
|
230
243
|
}
|
|
231
|
-
|
|
232
|
-
return lines.join('\n')
|
|
244
|
+
|
|
245
|
+
return lines.join('\n')
|
|
233
246
|
}
|