hone-ai 0.5.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 +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/run.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { existsSync } from 'fs'
|
|
2
|
-
import { resolve } from 'path'
|
|
3
|
-
import type { AgentType } from './config'
|
|
4
|
-
import { loadConfig, resolveModelForPhase } from './config'
|
|
5
|
-
import { spawnAgent, isAgentAvailable } from './agent'
|
|
6
|
-
import { constructPrompt, type PromptPhase } from './prompt'
|
|
7
|
-
import { exitWithError, ErrorMessages, parseAgentError } from './errors'
|
|
1
|
+
import { existsSync } from 'fs'
|
|
2
|
+
import { resolve } from 'path'
|
|
3
|
+
import type { AgentType } from './config'
|
|
4
|
+
import { loadConfig, resolveModelForPhase } from './config'
|
|
5
|
+
import { spawnAgent, isAgentAvailable } from './agent'
|
|
6
|
+
import { constructPrompt, type PromptPhase } from './prompt'
|
|
7
|
+
import { exitWithError, ErrorMessages, parseAgentError } from './errors'
|
|
8
8
|
|
|
9
9
|
export interface ExecuteTasksOptions {
|
|
10
|
-
tasksFile: string
|
|
11
|
-
iterations: number
|
|
12
|
-
agent: AgentType
|
|
13
|
-
skipPhase?: 'review'
|
|
10
|
+
tasksFile: string
|
|
11
|
+
iterations: number
|
|
12
|
+
agent: AgentType
|
|
13
|
+
skipPhase?: 'review'
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -18,226 +18,226 @@ export interface ExecuteTasksOptions {
|
|
|
18
18
|
* Runs for the specified number of iterations or until all tasks complete.
|
|
19
19
|
*/
|
|
20
20
|
export async function executeTasks(options: ExecuteTasksOptions): Promise<void> {
|
|
21
|
-
const { tasksFile, iterations, agent, skipPhase } = options
|
|
22
|
-
|
|
21
|
+
const { tasksFile, iterations, agent, skipPhase } = options
|
|
22
|
+
|
|
23
23
|
// Validate tasks file exists
|
|
24
|
-
const tasksPath = resolve(tasksFile)
|
|
24
|
+
const tasksPath = resolve(tasksFile)
|
|
25
25
|
if (!existsSync(tasksPath)) {
|
|
26
|
-
const { message, details } = ErrorMessages.FILE_NOT_FOUND(tasksPath)
|
|
27
|
-
exitWithError(message, details)
|
|
26
|
+
const { message, details } = ErrorMessages.FILE_NOT_FOUND(tasksPath)
|
|
27
|
+
exitWithError(message, details)
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
|
|
30
30
|
// Validate iterations is a positive integer
|
|
31
31
|
if (!Number.isInteger(iterations) || iterations < 1) {
|
|
32
|
-
throw new Error('Iterations must be a positive integer')
|
|
32
|
+
throw new Error('Iterations must be a positive integer')
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
// Check if agent is available
|
|
36
|
-
const available = await isAgentAvailable(agent)
|
|
36
|
+
const available = await isAgentAvailable(agent)
|
|
37
37
|
if (!available) {
|
|
38
|
-
const { message, details } = ErrorMessages.AGENT_NOT_FOUND(agent)
|
|
39
|
-
exitWithError(message, details)
|
|
38
|
+
const { message, details } = ErrorMessages.AGENT_NOT_FOUND(agent)
|
|
39
|
+
exitWithError(message, details)
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
// Extract feature name from tasks file name
|
|
43
|
-
const featureName = extractFeatureName(tasksPath)
|
|
43
|
+
const featureName = extractFeatureName(tasksPath)
|
|
44
44
|
if (!featureName) {
|
|
45
|
-
throw new Error(`Could not extract feature name from tasks file: ${tasksFile}`)
|
|
45
|
+
throw new Error(`Could not extract feature name from tasks file: ${tasksFile}`)
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
|
|
48
48
|
// Load config
|
|
49
|
-
const config = await loadConfig()
|
|
50
|
-
|
|
51
|
-
console.log(`Using agent: ${agent}`)
|
|
52
|
-
console.log(`Tasks file: ${tasksFile}`)
|
|
53
|
-
console.log(`Feature: ${featureName}`)
|
|
54
|
-
console.log(`Iterations: ${iterations}`)
|
|
49
|
+
const config = await loadConfig()
|
|
50
|
+
|
|
51
|
+
console.log(`Using agent: ${agent}`)
|
|
52
|
+
console.log(`Tasks file: ${tasksFile}`)
|
|
53
|
+
console.log(`Feature: ${featureName}`)
|
|
54
|
+
console.log(`Iterations: ${iterations}`)
|
|
55
55
|
if (skipPhase) {
|
|
56
|
-
console.log(`Skipping phase: ${skipPhase}`)
|
|
56
|
+
console.log(`Skipping phase: ${skipPhase}`)
|
|
57
57
|
}
|
|
58
|
-
console.log('')
|
|
59
|
-
|
|
58
|
+
console.log('')
|
|
59
|
+
|
|
60
60
|
// Main iteration loop
|
|
61
61
|
for (let i = 1; i <= iterations; i++) {
|
|
62
|
-
console.log(`\n${'='.repeat(80)}`)
|
|
63
|
-
console.log(`ITERATION ${i}/${iterations}`)
|
|
64
|
-
console.log('='.repeat(80))
|
|
65
|
-
console.log('')
|
|
66
|
-
|
|
62
|
+
console.log(`\n${'='.repeat(80)}`)
|
|
63
|
+
console.log(`ITERATION ${i}/${iterations}`)
|
|
64
|
+
console.log('='.repeat(80))
|
|
65
|
+
console.log('')
|
|
66
|
+
|
|
67
67
|
// Phase 1: Implement
|
|
68
|
-
console.log('Phase 1: Implement')
|
|
69
|
-
console.log('-'.repeat(80))
|
|
68
|
+
console.log('Phase 1: Implement')
|
|
69
|
+
console.log('-'.repeat(80))
|
|
70
70
|
const implementPrompt = constructPrompt({
|
|
71
71
|
phase: 'implement',
|
|
72
72
|
featureName,
|
|
73
|
-
config
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
const implementModel = resolveModelForPhase(config, 'implement', agent)
|
|
77
|
-
|
|
73
|
+
config,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const implementModel = resolveModelForPhase(config, 'implement', agent)
|
|
77
|
+
|
|
78
78
|
const implementResult = await spawnAgent({
|
|
79
79
|
agent,
|
|
80
80
|
prompt: implementPrompt,
|
|
81
81
|
workingDir: process.cwd(),
|
|
82
|
-
model: implementModel
|
|
83
|
-
})
|
|
84
|
-
|
|
82
|
+
model: implementModel,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
85
|
if (implementResult.exitCode !== 0) {
|
|
86
86
|
// Parse error type for better user feedback
|
|
87
|
-
const errorInfo = parseAgentError(implementResult.stderr, implementResult.exitCode)
|
|
88
|
-
|
|
87
|
+
const errorInfo = parseAgentError(implementResult.stderr, implementResult.exitCode)
|
|
88
|
+
|
|
89
89
|
if (errorInfo.type === 'model_unavailable') {
|
|
90
|
-
const { message, details } = ErrorMessages.MODEL_UNAVAILABLE(implementModel, agent)
|
|
91
|
-
exitWithError(message, details)
|
|
90
|
+
const { message, details } = ErrorMessages.MODEL_UNAVAILABLE(implementModel, agent)
|
|
91
|
+
exitWithError(message, details)
|
|
92
92
|
} else if (errorInfo.type === 'rate_limit') {
|
|
93
|
-
const { message, details } = ErrorMessages.RATE_LIMIT_ERROR(agent, errorInfo.retryAfter)
|
|
94
|
-
exitWithError(message, details)
|
|
93
|
+
const { message, details } = ErrorMessages.RATE_LIMIT_ERROR(agent, errorInfo.retryAfter)
|
|
94
|
+
exitWithError(message, details)
|
|
95
95
|
} else if (errorInfo.type === 'spawn_failed') {
|
|
96
|
-
const { message, details } = ErrorMessages.AGENT_SPAWN_FAILED(agent, implementResult.stderr)
|
|
97
|
-
exitWithError(message, details)
|
|
96
|
+
const { message, details } = ErrorMessages.AGENT_SPAWN_FAILED(agent, implementResult.stderr)
|
|
97
|
+
exitWithError(message, details)
|
|
98
98
|
}
|
|
99
|
-
|
|
99
|
+
|
|
100
100
|
// Generic error message for other failures
|
|
101
|
-
console.error('\n✗ Implement phase failed')
|
|
102
|
-
console.error('\nThe agent encountered an error during task implementation.')
|
|
103
|
-
console.error('The task has NOT been marked as completed.')
|
|
104
|
-
console.error('When you run hone again, it will retry the same task.')
|
|
105
|
-
console.error(`\nAgent exit code: ${implementResult.exitCode}`)
|
|
101
|
+
console.error('\n✗ Implement phase failed')
|
|
102
|
+
console.error('\nThe agent encountered an error during task implementation.')
|
|
103
|
+
console.error('The task has NOT been marked as completed.')
|
|
104
|
+
console.error('When you run hone again, it will retry the same task.')
|
|
105
|
+
console.error(`\nAgent exit code: ${implementResult.exitCode}`)
|
|
106
106
|
if (implementResult.stderr) {
|
|
107
|
-
console.error('\nError output:')
|
|
108
|
-
console.error(implementResult.stderr)
|
|
107
|
+
console.error('\nError output:')
|
|
108
|
+
console.error(implementResult.stderr)
|
|
109
109
|
}
|
|
110
|
-
throw new Error(`Implement phase failed with exit code ${implementResult.exitCode}`)
|
|
110
|
+
throw new Error(`Implement phase failed with exit code ${implementResult.exitCode}`)
|
|
111
111
|
}
|
|
112
|
-
|
|
112
|
+
|
|
113
113
|
// Extract task ID from output
|
|
114
|
-
const completedTaskId = extractTaskId(implementResult.stdout, 'TASK_COMPLETED')
|
|
114
|
+
const completedTaskId = extractTaskId(implementResult.stdout, 'TASK_COMPLETED')
|
|
115
115
|
if (completedTaskId) {
|
|
116
|
-
console.log(`\n✓ Task ${completedTaskId} implementation complete`)
|
|
116
|
+
console.log(`\n✓ Task ${completedTaskId} implementation complete`)
|
|
117
117
|
} else {
|
|
118
|
-
console.warn('\n⚠ Warning: No TASK_COMPLETED marker found in output')
|
|
118
|
+
console.warn('\n⚠ Warning: No TASK_COMPLETED marker found in output')
|
|
119
119
|
}
|
|
120
|
-
|
|
120
|
+
|
|
121
121
|
// Check if all tasks are complete
|
|
122
122
|
if (implementResult.stdout.includes('<promise>COMPLETE</promise>')) {
|
|
123
|
-
console.log('\n✓ All tasks completed!')
|
|
124
|
-
return
|
|
123
|
+
console.log('\n✓ All tasks completed!')
|
|
124
|
+
return
|
|
125
125
|
}
|
|
126
|
-
|
|
126
|
+
|
|
127
127
|
// Phase 2: Review (unless skipped)
|
|
128
|
-
let reviewFeedback: string | undefined
|
|
128
|
+
let reviewFeedback: string | undefined
|
|
129
129
|
if (skipPhase !== 'review') {
|
|
130
|
-
console.log('\nPhase 2: Review')
|
|
131
|
-
console.log('-'.repeat(80))
|
|
130
|
+
console.log('\nPhase 2: Review')
|
|
131
|
+
console.log('-'.repeat(80))
|
|
132
132
|
const reviewPrompt = constructPrompt({
|
|
133
133
|
phase: 'review',
|
|
134
134
|
featureName,
|
|
135
135
|
config,
|
|
136
|
-
taskId: completedTaskId
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
const reviewModel = resolveModelForPhase(config, 'review', agent)
|
|
140
|
-
|
|
136
|
+
taskId: completedTaskId,
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
const reviewModel = resolveModelForPhase(config, 'review', agent)
|
|
140
|
+
|
|
141
141
|
const reviewResult = await spawnAgent({
|
|
142
142
|
agent,
|
|
143
143
|
prompt: reviewPrompt,
|
|
144
144
|
workingDir: process.cwd(),
|
|
145
|
-
model: reviewModel
|
|
146
|
-
})
|
|
147
|
-
|
|
145
|
+
model: reviewModel,
|
|
146
|
+
})
|
|
147
|
+
|
|
148
148
|
if (reviewResult.exitCode !== 0) {
|
|
149
149
|
// Parse error type for better user feedback
|
|
150
|
-
const errorInfo = parseAgentError(reviewResult.stderr, reviewResult.exitCode)
|
|
151
|
-
|
|
150
|
+
const errorInfo = parseAgentError(reviewResult.stderr, reviewResult.exitCode)
|
|
151
|
+
|
|
152
152
|
if (errorInfo.type === 'model_unavailable') {
|
|
153
|
-
const { message, details } = ErrorMessages.MODEL_UNAVAILABLE(reviewModel, agent)
|
|
154
|
-
exitWithError(message, details)
|
|
153
|
+
const { message, details } = ErrorMessages.MODEL_UNAVAILABLE(reviewModel, agent)
|
|
154
|
+
exitWithError(message, details)
|
|
155
155
|
} else if (errorInfo.type === 'rate_limit') {
|
|
156
|
-
const { message, details } = ErrorMessages.RATE_LIMIT_ERROR(agent, errorInfo.retryAfter)
|
|
157
|
-
exitWithError(message, details)
|
|
156
|
+
const { message, details } = ErrorMessages.RATE_LIMIT_ERROR(agent, errorInfo.retryAfter)
|
|
157
|
+
exitWithError(message, details)
|
|
158
158
|
} else if (errorInfo.type === 'spawn_failed') {
|
|
159
|
-
const { message, details } = ErrorMessages.AGENT_SPAWN_FAILED(agent, reviewResult.stderr)
|
|
160
|
-
exitWithError(message, details)
|
|
159
|
+
const { message, details } = ErrorMessages.AGENT_SPAWN_FAILED(agent, reviewResult.stderr)
|
|
160
|
+
exitWithError(message, details)
|
|
161
161
|
}
|
|
162
|
-
|
|
162
|
+
|
|
163
163
|
// Generic error message for other failures
|
|
164
|
-
console.error('\n✗ Review phase failed')
|
|
165
|
-
console.error('\nThe agent encountered an error during task review.')
|
|
166
|
-
console.error('The task has NOT been marked as completed.')
|
|
167
|
-
console.error('When you run hone again, it will retry the same task.')
|
|
168
|
-
console.error(`\nAgent exit code: ${reviewResult.exitCode}`)
|
|
164
|
+
console.error('\n✗ Review phase failed')
|
|
165
|
+
console.error('\nThe agent encountered an error during task review.')
|
|
166
|
+
console.error('The task has NOT been marked as completed.')
|
|
167
|
+
console.error('When you run hone again, it will retry the same task.')
|
|
168
|
+
console.error(`\nAgent exit code: ${reviewResult.exitCode}`)
|
|
169
169
|
if (reviewResult.stderr) {
|
|
170
|
-
console.error('\nError output:')
|
|
171
|
-
console.error(reviewResult.stderr)
|
|
170
|
+
console.error('\nError output:')
|
|
171
|
+
console.error(reviewResult.stderr)
|
|
172
172
|
}
|
|
173
|
-
throw new Error(`Review phase failed with exit code ${reviewResult.exitCode}`)
|
|
173
|
+
throw new Error(`Review phase failed with exit code ${reviewResult.exitCode}`)
|
|
174
174
|
}
|
|
175
|
-
|
|
176
|
-
reviewFeedback = reviewResult.stdout
|
|
175
|
+
|
|
176
|
+
reviewFeedback = reviewResult.stdout
|
|
177
177
|
} else {
|
|
178
|
-
console.log('\nPhase 2: Review (skipped)')
|
|
178
|
+
console.log('\nPhase 2: Review (skipped)')
|
|
179
179
|
}
|
|
180
|
-
|
|
180
|
+
|
|
181
181
|
// Phase 3: Finalize
|
|
182
|
-
console.log('\nPhase 3: Finalize')
|
|
183
|
-
console.log('-'.repeat(80))
|
|
182
|
+
console.log('\nPhase 3: Finalize')
|
|
183
|
+
console.log('-'.repeat(80))
|
|
184
184
|
const finalizePrompt = constructPrompt({
|
|
185
185
|
phase: 'finalize',
|
|
186
186
|
featureName,
|
|
187
187
|
config,
|
|
188
188
|
taskId: completedTaskId,
|
|
189
|
-
reviewFeedback
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
const finalizeModel = resolveModelForPhase(config, 'finalize', agent)
|
|
193
|
-
|
|
189
|
+
reviewFeedback,
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
const finalizeModel = resolveModelForPhase(config, 'finalize', agent)
|
|
193
|
+
|
|
194
194
|
const finalizeResult = await spawnAgent({
|
|
195
195
|
agent,
|
|
196
196
|
prompt: finalizePrompt,
|
|
197
197
|
workingDir: process.cwd(),
|
|
198
|
-
model: finalizeModel
|
|
199
|
-
})
|
|
200
|
-
|
|
198
|
+
model: finalizeModel,
|
|
199
|
+
})
|
|
200
|
+
|
|
201
201
|
if (finalizeResult.exitCode !== 0) {
|
|
202
202
|
// Parse error type for better user feedback
|
|
203
|
-
const errorInfo = parseAgentError(finalizeResult.stderr, finalizeResult.exitCode)
|
|
204
|
-
|
|
203
|
+
const errorInfo = parseAgentError(finalizeResult.stderr, finalizeResult.exitCode)
|
|
204
|
+
|
|
205
205
|
if (errorInfo.type === 'model_unavailable') {
|
|
206
|
-
const { message, details } = ErrorMessages.MODEL_UNAVAILABLE(finalizeModel, agent)
|
|
207
|
-
exitWithError(message, details)
|
|
206
|
+
const { message, details } = ErrorMessages.MODEL_UNAVAILABLE(finalizeModel, agent)
|
|
207
|
+
exitWithError(message, details)
|
|
208
208
|
} else if (errorInfo.type === 'rate_limit') {
|
|
209
|
-
const { message, details } = ErrorMessages.RATE_LIMIT_ERROR(agent, errorInfo.retryAfter)
|
|
210
|
-
exitWithError(message, details)
|
|
209
|
+
const { message, details } = ErrorMessages.RATE_LIMIT_ERROR(agent, errorInfo.retryAfter)
|
|
210
|
+
exitWithError(message, details)
|
|
211
211
|
} else if (errorInfo.type === 'spawn_failed') {
|
|
212
|
-
const { message, details } = ErrorMessages.AGENT_SPAWN_FAILED(agent, finalizeResult.stderr)
|
|
213
|
-
exitWithError(message, details)
|
|
212
|
+
const { message, details } = ErrorMessages.AGENT_SPAWN_FAILED(agent, finalizeResult.stderr)
|
|
213
|
+
exitWithError(message, details)
|
|
214
214
|
}
|
|
215
|
-
|
|
215
|
+
|
|
216
216
|
// Generic error message for other failures
|
|
217
|
-
console.error('\n✗ Finalize phase failed')
|
|
218
|
-
console.error('\nThe agent encountered an error during task finalization.')
|
|
219
|
-
console.error('The task may not have been properly committed or marked as completed.')
|
|
220
|
-
console.error('Review the git status and task file manually before continuing.')
|
|
221
|
-
console.error(`\nAgent exit code: ${finalizeResult.exitCode}`)
|
|
217
|
+
console.error('\n✗ Finalize phase failed')
|
|
218
|
+
console.error('\nThe agent encountered an error during task finalization.')
|
|
219
|
+
console.error('The task may not have been properly committed or marked as completed.')
|
|
220
|
+
console.error('Review the git status and task file manually before continuing.')
|
|
221
|
+
console.error(`\nAgent exit code: ${finalizeResult.exitCode}`)
|
|
222
222
|
if (finalizeResult.stderr) {
|
|
223
|
-
console.error('\nError output:')
|
|
224
|
-
console.error(finalizeResult.stderr)
|
|
223
|
+
console.error('\nError output:')
|
|
224
|
+
console.error(finalizeResult.stderr)
|
|
225
225
|
}
|
|
226
|
-
throw new Error(`Finalize phase failed with exit code ${finalizeResult.exitCode}`)
|
|
226
|
+
throw new Error(`Finalize phase failed with exit code ${finalizeResult.exitCode}`)
|
|
227
227
|
}
|
|
228
|
-
|
|
228
|
+
|
|
229
229
|
// Verify task was finalized
|
|
230
|
-
const finalizedTaskId = extractTaskId(finalizeResult.stdout, 'FINALIZED')
|
|
230
|
+
const finalizedTaskId = extractTaskId(finalizeResult.stdout, 'FINALIZED')
|
|
231
231
|
if (finalizedTaskId) {
|
|
232
|
-
console.log(`\n✓ Iteration ${i} complete - Task ${finalizedTaskId} finalized`)
|
|
232
|
+
console.log(`\n✓ Iteration ${i} complete - Task ${finalizedTaskId} finalized`)
|
|
233
233
|
} else {
|
|
234
|
-
console.log(`\n✓ Iteration ${i} complete`)
|
|
234
|
+
console.log(`\n✓ Iteration ${i} complete`)
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
|
-
|
|
238
|
-
console.log(`\n${'='.repeat(80)}`)
|
|
239
|
-
console.log(`Completed ${iterations} iterations`)
|
|
240
|
-
console.log('='.repeat(80))
|
|
237
|
+
|
|
238
|
+
console.log(`\n${'='.repeat(80)}`)
|
|
239
|
+
console.log(`Completed ${iterations} iterations`)
|
|
240
|
+
console.log('='.repeat(80))
|
|
241
241
|
}
|
|
242
242
|
|
|
243
243
|
/**
|
|
@@ -245,8 +245,8 @@ export async function executeTasks(options: ExecuteTasksOptions): Promise<void>
|
|
|
245
245
|
* Expected format: tasks-<feature-name>.yml or .plans/tasks-<feature-name>.yml
|
|
246
246
|
*/
|
|
247
247
|
function extractFeatureName(tasksPath: string): string | undefined {
|
|
248
|
-
const match = tasksPath.match(/tasks-([^/]+)\.yml$/)
|
|
249
|
-
return match ? match[1] : undefined
|
|
248
|
+
const match = tasksPath.match(/tasks-([^/]+)\.yml$/)
|
|
249
|
+
return match ? match[1] : undefined
|
|
250
250
|
}
|
|
251
251
|
|
|
252
252
|
/**
|
|
@@ -254,7 +254,7 @@ function extractFeatureName(tasksPath: string): string | undefined {
|
|
|
254
254
|
* Looks for patterns like "TASK_COMPLETED: task-123" or "FINALIZED: task-123"
|
|
255
255
|
*/
|
|
256
256
|
function extractTaskId(output: string, marker: 'TASK_COMPLETED' | 'FINALIZED'): string | undefined {
|
|
257
|
-
const regex = new RegExp(`${marker}:\\s*([\\w-]+)`, 'i')
|
|
258
|
-
const match = output.match(regex)
|
|
259
|
-
return match ? match[1] : undefined
|
|
257
|
+
const regex = new RegExp(`${marker}:\\s*([\\w-]+)`, 'i')
|
|
258
|
+
const match = output.match(regex)
|
|
259
|
+
return match ? match[1] : undefined
|
|
260
260
|
}
|
package/src/status.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { describe, test, expect } from 'bun:test'
|
|
2
|
-
import { findNextTask } from './status'
|
|
3
|
-
import type { TaskFile } from './prds'
|
|
1
|
+
import { describe, test, expect } from 'bun:test'
|
|
2
|
+
import { findNextTask } from './status'
|
|
3
|
+
import type { TaskFile } from './prds'
|
|
4
4
|
|
|
5
5
|
describe('status', () => {
|
|
6
6
|
describe('findNextTask', () => {
|
|
@@ -16,22 +16,22 @@ describe('status', () => {
|
|
|
16
16
|
title: 'Task 1',
|
|
17
17
|
description: 'First task',
|
|
18
18
|
status: 'pending',
|
|
19
|
-
dependencies: []
|
|
19
|
+
dependencies: [],
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
id: 'task-2',
|
|
23
23
|
title: 'Task 2',
|
|
24
24
|
description: 'Second task',
|
|
25
25
|
status: 'pending',
|
|
26
|
-
dependencies: []
|
|
27
|
-
}
|
|
28
|
-
]
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const next = findNextTask(taskFile)
|
|
32
|
-
expect(next?.id).toBe('task-1')
|
|
33
|
-
})
|
|
34
|
-
|
|
26
|
+
dependencies: [],
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const next = findNextTask(taskFile)
|
|
32
|
+
expect(next?.id).toBe('task-1')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
35
|
test('skips pending task with incomplete dependencies', () => {
|
|
36
36
|
const taskFile: TaskFile = {
|
|
37
37
|
feature: 'test',
|
|
@@ -44,22 +44,22 @@ describe('status', () => {
|
|
|
44
44
|
title: 'Task 1',
|
|
45
45
|
description: 'First task',
|
|
46
46
|
status: 'pending',
|
|
47
|
-
dependencies: []
|
|
47
|
+
dependencies: [],
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
id: 'task-2',
|
|
51
51
|
title: 'Task 2',
|
|
52
52
|
description: 'Second task',
|
|
53
53
|
status: 'pending',
|
|
54
|
-
dependencies: ['task-1']
|
|
55
|
-
}
|
|
56
|
-
]
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const next = findNextTask(taskFile)
|
|
60
|
-
expect(next?.id).toBe('task-1')
|
|
61
|
-
})
|
|
62
|
-
|
|
54
|
+
dependencies: ['task-1'],
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const next = findNextTask(taskFile)
|
|
60
|
+
expect(next?.id).toBe('task-1')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
63
|
test('returns pending task when dependencies completed', () => {
|
|
64
64
|
const taskFile: TaskFile = {
|
|
65
65
|
feature: 'test',
|
|
@@ -72,22 +72,22 @@ describe('status', () => {
|
|
|
72
72
|
title: 'Task 1',
|
|
73
73
|
description: 'First task',
|
|
74
74
|
status: 'completed',
|
|
75
|
-
dependencies: []
|
|
75
|
+
dependencies: [],
|
|
76
76
|
},
|
|
77
77
|
{
|
|
78
78
|
id: 'task-2',
|
|
79
79
|
title: 'Task 2',
|
|
80
80
|
description: 'Second task',
|
|
81
81
|
status: 'pending',
|
|
82
|
-
dependencies: ['task-1']
|
|
83
|
-
}
|
|
84
|
-
]
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const next = findNextTask(taskFile)
|
|
88
|
-
expect(next?.id).toBe('task-2')
|
|
89
|
-
})
|
|
90
|
-
|
|
82
|
+
dependencies: ['task-1'],
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const next = findNextTask(taskFile)
|
|
88
|
+
expect(next?.id).toBe('task-2')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
91
|
test('returns null when no pending tasks', () => {
|
|
92
92
|
const taskFile: TaskFile = {
|
|
93
93
|
feature: 'test',
|
|
@@ -100,15 +100,15 @@ describe('status', () => {
|
|
|
100
100
|
title: 'Task 1',
|
|
101
101
|
description: 'First task',
|
|
102
102
|
status: 'completed',
|
|
103
|
-
dependencies: []
|
|
104
|
-
}
|
|
105
|
-
]
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const next = findNextTask(taskFile)
|
|
109
|
-
expect(next).toBeNull()
|
|
110
|
-
})
|
|
111
|
-
|
|
103
|
+
dependencies: [],
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const next = findNextTask(taskFile)
|
|
109
|
+
expect(next).toBeNull()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
112
|
test('returns null when all pending tasks blocked by dependencies', () => {
|
|
113
113
|
const taskFile: TaskFile = {
|
|
114
114
|
feature: 'test',
|
|
@@ -121,39 +121,39 @@ describe('status', () => {
|
|
|
121
121
|
title: 'Task 1',
|
|
122
122
|
description: 'First task',
|
|
123
123
|
status: 'pending',
|
|
124
|
-
dependencies: []
|
|
124
|
+
dependencies: [],
|
|
125
125
|
},
|
|
126
126
|
{
|
|
127
127
|
id: 'task-2',
|
|
128
128
|
title: 'Task 2',
|
|
129
129
|
description: 'Second task',
|
|
130
130
|
status: 'pending',
|
|
131
|
-
dependencies: ['task-1']
|
|
132
|
-
}
|
|
133
|
-
]
|
|
134
|
-
}
|
|
135
|
-
|
|
131
|
+
dependencies: ['task-1'],
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
}
|
|
135
|
+
|
|
136
136
|
// Mark task-1 as something other than completed
|
|
137
137
|
if (taskFile.tasks[0]) {
|
|
138
|
-
taskFile.tasks[0].status = 'in_progress'
|
|
138
|
+
taskFile.tasks[0].status = 'in_progress'
|
|
139
139
|
}
|
|
140
|
-
|
|
141
|
-
const next = findNextTask(taskFile)
|
|
142
|
-
expect(next).toBeNull()
|
|
143
|
-
})
|
|
144
|
-
|
|
140
|
+
|
|
141
|
+
const next = findNextTask(taskFile)
|
|
142
|
+
expect(next).toBeNull()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
145
|
test('handles empty task list', () => {
|
|
146
146
|
const taskFile: TaskFile = {
|
|
147
147
|
feature: 'test',
|
|
148
148
|
prd: './prd-test.md',
|
|
149
149
|
created_at: '2026-01-28',
|
|
150
150
|
updated_at: '2026-01-28',
|
|
151
|
-
tasks: []
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const next = findNextTask(taskFile)
|
|
155
|
-
expect(next).toBeNull()
|
|
156
|
-
})
|
|
151
|
+
tasks: [],
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const next = findNextTask(taskFile)
|
|
155
|
+
expect(next).toBeNull()
|
|
156
|
+
})
|
|
157
157
|
|
|
158
158
|
test('treats cancelled tasks as satisfied dependencies', () => {
|
|
159
159
|
const taskFile: TaskFile = {
|
|
@@ -167,21 +167,21 @@ describe('status', () => {
|
|
|
167
167
|
title: 'Task 1',
|
|
168
168
|
description: 'First task',
|
|
169
169
|
status: 'cancelled',
|
|
170
|
-
dependencies: []
|
|
170
|
+
dependencies: [],
|
|
171
171
|
},
|
|
172
172
|
{
|
|
173
173
|
id: 'task-2',
|
|
174
174
|
title: 'Task 2',
|
|
175
175
|
description: 'Second task',
|
|
176
176
|
status: 'pending',
|
|
177
|
-
dependencies: ['task-1']
|
|
178
|
-
}
|
|
179
|
-
]
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const next = findNextTask(taskFile)
|
|
183
|
-
expect(next?.id).toBe('task-2')
|
|
184
|
-
})
|
|
177
|
+
dependencies: ['task-1'],
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const next = findNextTask(taskFile)
|
|
183
|
+
expect(next?.id).toBe('task-2')
|
|
184
|
+
})
|
|
185
185
|
|
|
186
186
|
test('skips cancelled tasks when searching for next', () => {
|
|
187
187
|
const taskFile: TaskFile = {
|
|
@@ -195,20 +195,20 @@ describe('status', () => {
|
|
|
195
195
|
title: 'Task 1',
|
|
196
196
|
description: 'First task',
|
|
197
197
|
status: 'cancelled',
|
|
198
|
-
dependencies: []
|
|
198
|
+
dependencies: [],
|
|
199
199
|
},
|
|
200
200
|
{
|
|
201
201
|
id: 'task-2',
|
|
202
202
|
title: 'Task 2',
|
|
203
203
|
description: 'Second task',
|
|
204
204
|
status: 'pending',
|
|
205
|
-
dependencies: []
|
|
206
|
-
}
|
|
207
|
-
]
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const next = findNextTask(taskFile)
|
|
211
|
-
expect(next?.id).toBe('task-2')
|
|
212
|
-
})
|
|
213
|
-
})
|
|
214
|
-
})
|
|
205
|
+
dependencies: [],
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const next = findNextTask(taskFile)
|
|
211
|
+
expect(next?.id).toBe('task-2')
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
})
|