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/prds.test.ts
CHANGED
|
@@ -1,26 +1,21 @@
|
|
|
1
|
-
import { describe, test, expect } from 'bun:test'
|
|
2
|
-
import {
|
|
3
|
-
extractFeatureName,
|
|
4
|
-
calculateStatus,
|
|
5
|
-
type TaskFile,
|
|
6
|
-
type Task
|
|
7
|
-
} from './prds';
|
|
1
|
+
import { describe, test, expect } from 'bun:test'
|
|
2
|
+
import { extractFeatureName, calculateStatus, type TaskFile, type Task } from './prds'
|
|
8
3
|
|
|
9
4
|
describe('extractFeatureName', () => {
|
|
10
5
|
test('extracts feature name from PRD filename', () => {
|
|
11
|
-
expect(extractFeatureName('prd-hone.md')).toBe('hone')
|
|
12
|
-
expect(extractFeatureName('prd-delete-button.md')).toBe('delete-button')
|
|
13
|
-
expect(extractFeatureName('prd-user-auth.md')).toBe('user-auth')
|
|
14
|
-
})
|
|
15
|
-
})
|
|
6
|
+
expect(extractFeatureName('prd-hone.md')).toBe('hone')
|
|
7
|
+
expect(extractFeatureName('prd-delete-button.md')).toBe('delete-button')
|
|
8
|
+
expect(extractFeatureName('prd-user-auth.md')).toBe('user-auth')
|
|
9
|
+
})
|
|
10
|
+
})
|
|
16
11
|
|
|
17
12
|
describe('calculateStatus', () => {
|
|
18
13
|
test('returns "not started" when task file is null', () => {
|
|
19
|
-
const result = calculateStatus(null)
|
|
20
|
-
expect(result.status).toBe('not started')
|
|
21
|
-
expect(result.completedCount).toBe(0)
|
|
22
|
-
expect(result.totalCount).toBe(0)
|
|
23
|
-
})
|
|
14
|
+
const result = calculateStatus(null)
|
|
15
|
+
expect(result.status).toBe('not started')
|
|
16
|
+
expect(result.completedCount).toBe(0)
|
|
17
|
+
expect(result.totalCount).toBe(0)
|
|
18
|
+
})
|
|
24
19
|
|
|
25
20
|
test('returns "not started" when no tasks exist', () => {
|
|
26
21
|
const taskFile: TaskFile = {
|
|
@@ -28,13 +23,13 @@ describe('calculateStatus', () => {
|
|
|
28
23
|
prd: 'prd-test.md',
|
|
29
24
|
created_at: '2026-01-28T12:00:00Z',
|
|
30
25
|
updated_at: '2026-01-28T12:00:00Z',
|
|
31
|
-
tasks: []
|
|
32
|
-
}
|
|
33
|
-
const result = calculateStatus(taskFile)
|
|
34
|
-
expect(result.status).toBe('not started')
|
|
35
|
-
expect(result.completedCount).toBe(0)
|
|
36
|
-
expect(result.totalCount).toBe(0)
|
|
37
|
-
})
|
|
26
|
+
tasks: [],
|
|
27
|
+
}
|
|
28
|
+
const result = calculateStatus(taskFile)
|
|
29
|
+
expect(result.status).toBe('not started')
|
|
30
|
+
expect(result.completedCount).toBe(0)
|
|
31
|
+
expect(result.totalCount).toBe(0)
|
|
32
|
+
})
|
|
38
33
|
|
|
39
34
|
test('returns "not started" when all tasks are pending', () => {
|
|
40
35
|
const taskFile: TaskFile = {
|
|
@@ -44,14 +39,14 @@ describe('calculateStatus', () => {
|
|
|
44
39
|
updated_at: '2026-01-28T12:00:00Z',
|
|
45
40
|
tasks: [
|
|
46
41
|
{ id: 'task-001', title: 'Task 1', description: 'Desc', status: 'pending' },
|
|
47
|
-
{ id: 'task-002', title: 'Task 2', description: 'Desc', status: 'pending' }
|
|
48
|
-
]
|
|
49
|
-
}
|
|
50
|
-
const result = calculateStatus(taskFile)
|
|
51
|
-
expect(result.status).toBe('not started')
|
|
52
|
-
expect(result.completedCount).toBe(0)
|
|
53
|
-
expect(result.totalCount).toBe(2)
|
|
54
|
-
})
|
|
42
|
+
{ id: 'task-002', title: 'Task 2', description: 'Desc', status: 'pending' },
|
|
43
|
+
],
|
|
44
|
+
}
|
|
45
|
+
const result = calculateStatus(taskFile)
|
|
46
|
+
expect(result.status).toBe('not started')
|
|
47
|
+
expect(result.completedCount).toBe(0)
|
|
48
|
+
expect(result.totalCount).toBe(2)
|
|
49
|
+
})
|
|
55
50
|
|
|
56
51
|
test('returns "in progress" when some tasks are completed', () => {
|
|
57
52
|
const taskFile: TaskFile = {
|
|
@@ -62,14 +57,14 @@ describe('calculateStatus', () => {
|
|
|
62
57
|
tasks: [
|
|
63
58
|
{ id: 'task-001', title: 'Task 1', description: 'Desc', status: 'completed' },
|
|
64
59
|
{ id: 'task-002', title: 'Task 2', description: 'Desc', status: 'pending' },
|
|
65
|
-
{ id: 'task-003', title: 'Task 3', description: 'Desc', status: 'pending' }
|
|
66
|
-
]
|
|
67
|
-
}
|
|
68
|
-
const result = calculateStatus(taskFile)
|
|
69
|
-
expect(result.status).toBe('in progress')
|
|
70
|
-
expect(result.completedCount).toBe(1)
|
|
71
|
-
expect(result.totalCount).toBe(3)
|
|
72
|
-
})
|
|
60
|
+
{ id: 'task-003', title: 'Task 3', description: 'Desc', status: 'pending' },
|
|
61
|
+
],
|
|
62
|
+
}
|
|
63
|
+
const result = calculateStatus(taskFile)
|
|
64
|
+
expect(result.status).toBe('in progress')
|
|
65
|
+
expect(result.completedCount).toBe(1)
|
|
66
|
+
expect(result.totalCount).toBe(3)
|
|
67
|
+
})
|
|
73
68
|
|
|
74
69
|
test('returns "completed" when all tasks are completed', () => {
|
|
75
70
|
const taskFile: TaskFile = {
|
|
@@ -79,14 +74,14 @@ describe('calculateStatus', () => {
|
|
|
79
74
|
updated_at: '2026-01-28T12:00:00Z',
|
|
80
75
|
tasks: [
|
|
81
76
|
{ id: 'task-001', title: 'Task 1', description: 'Desc', status: 'completed' },
|
|
82
|
-
{ id: 'task-002', title: 'Task 2', description: 'Desc', status: 'completed' }
|
|
83
|
-
]
|
|
84
|
-
}
|
|
85
|
-
const result = calculateStatus(taskFile)
|
|
86
|
-
expect(result.status).toBe('completed')
|
|
87
|
-
expect(result.completedCount).toBe(2)
|
|
88
|
-
expect(result.totalCount).toBe(2)
|
|
89
|
-
})
|
|
77
|
+
{ id: 'task-002', title: 'Task 2', description: 'Desc', status: 'completed' },
|
|
78
|
+
],
|
|
79
|
+
}
|
|
80
|
+
const result = calculateStatus(taskFile)
|
|
81
|
+
expect(result.status).toBe('completed')
|
|
82
|
+
expect(result.completedCount).toBe(2)
|
|
83
|
+
expect(result.totalCount).toBe(2)
|
|
84
|
+
})
|
|
90
85
|
|
|
91
86
|
test('counts cancelled tasks as completed', () => {
|
|
92
87
|
const taskFile: TaskFile = {
|
|
@@ -97,14 +92,14 @@ describe('calculateStatus', () => {
|
|
|
97
92
|
tasks: [
|
|
98
93
|
{ id: 'task-001', title: 'Task 1', description: 'Desc', status: 'completed' },
|
|
99
94
|
{ id: 'task-002', title: 'Task 2', description: 'Desc', status: 'cancelled' },
|
|
100
|
-
{ id: 'task-003', title: 'Task 3', description: 'Desc', status: 'pending' }
|
|
101
|
-
]
|
|
102
|
-
}
|
|
103
|
-
const result = calculateStatus(taskFile)
|
|
104
|
-
expect(result.status).toBe('in progress')
|
|
105
|
-
expect(result.completedCount).toBe(2)
|
|
106
|
-
expect(result.totalCount).toBe(3)
|
|
107
|
-
})
|
|
95
|
+
{ id: 'task-003', title: 'Task 3', description: 'Desc', status: 'pending' },
|
|
96
|
+
],
|
|
97
|
+
}
|
|
98
|
+
const result = calculateStatus(taskFile)
|
|
99
|
+
expect(result.status).toBe('in progress')
|
|
100
|
+
expect(result.completedCount).toBe(2)
|
|
101
|
+
expect(result.totalCount).toBe(3)
|
|
102
|
+
})
|
|
108
103
|
|
|
109
104
|
test('returns "completed" when all tasks are completed or cancelled', () => {
|
|
110
105
|
const taskFile: TaskFile = {
|
|
@@ -114,12 +109,12 @@ describe('calculateStatus', () => {
|
|
|
114
109
|
updated_at: '2026-01-28T12:00:00Z',
|
|
115
110
|
tasks: [
|
|
116
111
|
{ id: 'task-001', title: 'Task 1', description: 'Desc', status: 'completed' },
|
|
117
|
-
{ id: 'task-002', title: 'Task 2', description: 'Desc', status: 'cancelled' }
|
|
118
|
-
]
|
|
119
|
-
}
|
|
120
|
-
const result = calculateStatus(taskFile)
|
|
121
|
-
expect(result.status).toBe('completed')
|
|
122
|
-
expect(result.completedCount).toBe(2)
|
|
123
|
-
expect(result.totalCount).toBe(2)
|
|
124
|
-
})
|
|
125
|
-
})
|
|
112
|
+
{ id: 'task-002', title: 'Task 2', description: 'Desc', status: 'cancelled' },
|
|
113
|
+
],
|
|
114
|
+
}
|
|
115
|
+
const result = calculateStatus(taskFile)
|
|
116
|
+
expect(result.status).toBe('completed')
|
|
117
|
+
expect(result.completedCount).toBe(2)
|
|
118
|
+
expect(result.totalCount).toBe(2)
|
|
119
|
+
})
|
|
120
|
+
})
|
package/src/prds.ts
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import { readdirSync, existsSync } from 'fs'
|
|
2
|
-
import { readFile } from 'fs/promises'
|
|
3
|
-
import { join } from 'path'
|
|
4
|
-
import yaml from 'js-yaml'
|
|
5
|
-
import { getPlansDir } from './config'
|
|
1
|
+
import { readdirSync, existsSync } from 'fs'
|
|
2
|
+
import { readFile } from 'fs/promises'
|
|
3
|
+
import { join } from 'path'
|
|
4
|
+
import yaml from 'js-yaml'
|
|
5
|
+
import { getPlansDir } from './config'
|
|
6
6
|
|
|
7
7
|
export interface Task {
|
|
8
|
-
id: string
|
|
9
|
-
title: string
|
|
10
|
-
description: string
|
|
11
|
-
status: 'pending' | 'in_progress' | 'completed' | 'failed' | 'cancelled'
|
|
12
|
-
dependencies?: string[]
|
|
13
|
-
acceptance_criteria?: string[]
|
|
14
|
-
completed_at?: string | null
|
|
8
|
+
id: string
|
|
9
|
+
title: string
|
|
10
|
+
description: string
|
|
11
|
+
status: 'pending' | 'in_progress' | 'completed' | 'failed' | 'cancelled'
|
|
12
|
+
dependencies?: string[]
|
|
13
|
+
acceptance_criteria?: string[]
|
|
14
|
+
completed_at?: string | null
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export interface TaskFile {
|
|
18
|
-
feature: string
|
|
19
|
-
prd: string
|
|
20
|
-
created_at: string
|
|
21
|
-
updated_at: string
|
|
22
|
-
tasks: Task[]
|
|
18
|
+
feature: string
|
|
19
|
+
prd: string
|
|
20
|
+
created_at: string
|
|
21
|
+
updated_at: string
|
|
22
|
+
tasks: Task[]
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export interface PrdInfo {
|
|
26
|
-
filename: string
|
|
27
|
-
taskFile?: string
|
|
28
|
-
status: 'not started' | 'in progress' | 'completed'
|
|
29
|
-
completedCount?: number
|
|
30
|
-
totalCount?: number
|
|
26
|
+
filename: string
|
|
27
|
+
taskFile?: string
|
|
28
|
+
status: 'not started' | 'in progress' | 'completed'
|
|
29
|
+
completedCount?: number
|
|
30
|
+
totalCount?: number
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
@@ -35,40 +35,40 @@ export interface PrdInfo {
|
|
|
35
35
|
* e.g., "prd-hone.md" -> "hone"
|
|
36
36
|
*/
|
|
37
37
|
export function extractFeatureName(prdFilename: string): string {
|
|
38
|
-
return prdFilename.replace(/^prd-/, '').replace(/\.md$/, '')
|
|
38
|
+
return prdFilename.replace(/^prd-/, '').replace(/\.md$/, '')
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* Get all PRD files in .plans/ directory
|
|
43
43
|
*/
|
|
44
44
|
export function listPrdFiles(): string[] {
|
|
45
|
-
const plansDir = getPlansDir()
|
|
45
|
+
const plansDir = getPlansDir()
|
|
46
46
|
if (!existsSync(plansDir)) {
|
|
47
|
-
return []
|
|
47
|
+
return []
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
const files = readdirSync(plansDir)
|
|
51
|
-
return files.filter(file => file.startsWith('prd-') && file.endsWith('.md'))
|
|
49
|
+
|
|
50
|
+
const files = readdirSync(plansDir)
|
|
51
|
+
return files.filter(file => file.startsWith('prd-') && file.endsWith('.md'))
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
55
|
* Load and parse a task file
|
|
56
56
|
*/
|
|
57
57
|
export async function loadTaskFile(taskFilename: string): Promise<TaskFile | null> {
|
|
58
|
-
const plansDir = getPlansDir()
|
|
59
|
-
const taskPath = join(plansDir, taskFilename)
|
|
60
|
-
|
|
58
|
+
const plansDir = getPlansDir()
|
|
59
|
+
const taskPath = join(plansDir, taskFilename)
|
|
60
|
+
|
|
61
61
|
if (!existsSync(taskPath)) {
|
|
62
|
-
return null
|
|
62
|
+
return null
|
|
63
63
|
}
|
|
64
|
-
|
|
64
|
+
|
|
65
65
|
try {
|
|
66
|
-
const content = await readFile(taskPath, 'utf-8')
|
|
67
|
-
const parsed = yaml.load(content) as TaskFile
|
|
68
|
-
return parsed
|
|
66
|
+
const content = await readFile(taskPath, 'utf-8')
|
|
67
|
+
const parsed = yaml.load(content) as TaskFile
|
|
68
|
+
return parsed
|
|
69
69
|
} catch (error) {
|
|
70
|
-
console.error(`Error parsing task file ${taskFilename}:`, error)
|
|
71
|
-
return null
|
|
70
|
+
console.error(`Error parsing task file ${taskFilename}:`, error)
|
|
71
|
+
return null
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
@@ -77,23 +77,25 @@ export async function loadTaskFile(taskFilename: string): Promise<TaskFile | nul
|
|
|
77
77
|
* Cancelled tasks are counted towards completion
|
|
78
78
|
*/
|
|
79
79
|
export function calculateStatus(taskFile: TaskFile | null): {
|
|
80
|
-
status: 'not started' | 'in progress' | 'completed'
|
|
81
|
-
completedCount: number
|
|
82
|
-
totalCount: number
|
|
80
|
+
status: 'not started' | 'in progress' | 'completed'
|
|
81
|
+
completedCount: number
|
|
82
|
+
totalCount: number
|
|
83
83
|
} {
|
|
84
84
|
if (!taskFile || !taskFile.tasks || taskFile.tasks.length === 0) {
|
|
85
|
-
return { status: 'not started', completedCount: 0, totalCount: 0 }
|
|
85
|
+
return { status: 'not started', completedCount: 0, totalCount: 0 }
|
|
86
86
|
}
|
|
87
|
-
|
|
88
|
-
const totalCount = taskFile.tasks.length
|
|
89
|
-
const completedCount = taskFile.tasks.filter(
|
|
90
|
-
|
|
87
|
+
|
|
88
|
+
const totalCount = taskFile.tasks.length
|
|
89
|
+
const completedCount = taskFile.tasks.filter(
|
|
90
|
+
t => t.status === 'completed' || t.status === 'cancelled'
|
|
91
|
+
).length
|
|
92
|
+
|
|
91
93
|
if (completedCount === 0) {
|
|
92
|
-
return { status: 'not started', completedCount, totalCount }
|
|
94
|
+
return { status: 'not started', completedCount, totalCount }
|
|
93
95
|
} else if (completedCount === totalCount) {
|
|
94
|
-
return { status: 'completed', completedCount, totalCount }
|
|
96
|
+
return { status: 'completed', completedCount, totalCount }
|
|
95
97
|
} else {
|
|
96
|
-
return { status: 'in progress', completedCount, totalCount }
|
|
98
|
+
return { status: 'in progress', completedCount, totalCount }
|
|
97
99
|
}
|
|
98
100
|
}
|
|
99
101
|
|
|
@@ -101,28 +103,28 @@ export function calculateStatus(taskFile: TaskFile | null): {
|
|
|
101
103
|
* Get PRD info including status
|
|
102
104
|
*/
|
|
103
105
|
export async function getPrdInfo(prdFilename: string): Promise<PrdInfo> {
|
|
104
|
-
const featureName = extractFeatureName(prdFilename)
|
|
105
|
-
const taskFilename = `tasks-${featureName}.yml
|
|
106
|
-
const plansDir = getPlansDir()
|
|
107
|
-
const taskPath = join(plansDir, taskFilename)
|
|
108
|
-
|
|
109
|
-
const taskFile = existsSync(taskPath) ? await loadTaskFile(taskFilename) : null
|
|
110
|
-
const { status, completedCount, totalCount } = calculateStatus(taskFile)
|
|
111
|
-
|
|
106
|
+
const featureName = extractFeatureName(prdFilename)
|
|
107
|
+
const taskFilename = `tasks-${featureName}.yml`
|
|
108
|
+
const plansDir = getPlansDir()
|
|
109
|
+
const taskPath = join(plansDir, taskFilename)
|
|
110
|
+
|
|
111
|
+
const taskFile = existsSync(taskPath) ? await loadTaskFile(taskFilename) : null
|
|
112
|
+
const { status, completedCount, totalCount } = calculateStatus(taskFile)
|
|
113
|
+
|
|
112
114
|
return {
|
|
113
115
|
filename: prdFilename,
|
|
114
116
|
taskFile: taskFile ? taskFilename : undefined,
|
|
115
117
|
status,
|
|
116
118
|
completedCount: taskFile ? completedCount : undefined,
|
|
117
|
-
totalCount: taskFile ? totalCount : undefined
|
|
118
|
-
}
|
|
119
|
+
totalCount: taskFile ? totalCount : undefined,
|
|
120
|
+
}
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
/**
|
|
122
124
|
* List all PRDs with their info
|
|
123
125
|
*/
|
|
124
126
|
export async function listPrds(): Promise<PrdInfo[]> {
|
|
125
|
-
const prdFiles = listPrdFiles()
|
|
126
|
-
const prds = await Promise.all(prdFiles.map(file => getPrdInfo(file)))
|
|
127
|
-
return prds
|
|
127
|
+
const prdFiles = listPrdFiles()
|
|
128
|
+
const prds = await Promise.all(prdFiles.map(file => getPrdInfo(file)))
|
|
129
|
+
return prds
|
|
128
130
|
}
|