opencode-hive 0.8.0 → 0.8.2
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/dist/index.js +19251 -587
- package/package.json +4 -2
- package/dist/e2e/opencode-runtime-smoke.test.d.ts +0 -1
- package/dist/e2e/opencode-runtime-smoke.test.js +0 -243
- package/dist/e2e/plugin-smoke.test.d.ts +0 -1
- package/dist/e2e/plugin-smoke.test.js +0 -127
- package/dist/services/contextService.d.ts +0 -15
- package/dist/services/contextService.js +0 -59
- package/dist/services/featureService.d.ts +0 -14
- package/dist/services/featureService.js +0 -107
- package/dist/services/featureService.test.d.ts +0 -1
- package/dist/services/featureService.test.js +0 -127
- package/dist/services/index.d.ts +0 -5
- package/dist/services/index.js +0 -4
- package/dist/services/planService.d.ts +0 -11
- package/dist/services/planService.js +0 -59
- package/dist/services/planService.test.d.ts +0 -1
- package/dist/services/planService.test.js +0 -115
- package/dist/services/sessionService.d.ts +0 -31
- package/dist/services/sessionService.js +0 -125
- package/dist/services/taskService.d.ts +0 -29
- package/dist/services/taskService.js +0 -382
- package/dist/services/taskService.test.d.ts +0 -1
- package/dist/services/taskService.test.js +0 -290
- package/dist/services/worktreeService.d.ts +0 -66
- package/dist/services/worktreeService.js +0 -498
- package/dist/services/worktreeService.test.d.ts +0 -1
- package/dist/services/worktreeService.test.js +0 -185
- package/dist/tools/contextTools.d.ts +0 -93
- package/dist/tools/contextTools.js +0 -83
- package/dist/tools/execTools.d.ts +0 -66
- package/dist/tools/execTools.js +0 -125
- package/dist/tools/featureTools.d.ts +0 -60
- package/dist/tools/featureTools.js +0 -73
- package/dist/tools/planTools.d.ts +0 -47
- package/dist/tools/planTools.js +0 -65
- package/dist/tools/sessionTools.d.ts +0 -35
- package/dist/tools/sessionTools.js +0 -95
- package/dist/tools/taskTools.d.ts +0 -79
- package/dist/tools/taskTools.js +0 -86
- package/dist/types.d.ts +0 -106
- package/dist/types.js +0 -1
- package/dist/utils/detection.d.ts +0 -12
- package/dist/utils/detection.js +0 -73
- package/dist/utils/paths.d.ts +0 -23
- package/dist/utils/paths.js +0 -92
- package/dist/utils/paths.test.d.ts +0 -1
- package/dist/utils/paths.test.js +0 -100
|
@@ -1,382 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import { getTasksPath, getTaskPath, getTaskStatusPath, getTaskReportPath, getTaskSpecPath, getSubtasksPath, getSubtaskPath, getSubtaskStatusPath, getSubtaskSpecPath, getSubtaskReportPath, getPlanPath, ensureDir, readJson, writeJson, readText, writeText, fileExists, } from '../utils/paths.js';
|
|
3
|
-
export class TaskService {
|
|
4
|
-
projectRoot;
|
|
5
|
-
constructor(projectRoot) {
|
|
6
|
-
this.projectRoot = projectRoot;
|
|
7
|
-
}
|
|
8
|
-
sync(featureName) {
|
|
9
|
-
const planPath = getPlanPath(this.projectRoot, featureName);
|
|
10
|
-
const planContent = readText(planPath);
|
|
11
|
-
if (!planContent) {
|
|
12
|
-
throw new Error(`No plan.md found for feature '${featureName}'`);
|
|
13
|
-
}
|
|
14
|
-
const planTasks = this.parseTasksFromPlan(planContent);
|
|
15
|
-
const existingTasks = this.list(featureName);
|
|
16
|
-
const result = {
|
|
17
|
-
created: [],
|
|
18
|
-
removed: [],
|
|
19
|
-
kept: [],
|
|
20
|
-
manual: [],
|
|
21
|
-
};
|
|
22
|
-
const existingByName = new Map(existingTasks.map(t => [t.folder, t]));
|
|
23
|
-
for (const existing of existingTasks) {
|
|
24
|
-
if (existing.origin === 'manual') {
|
|
25
|
-
result.manual.push(existing.folder);
|
|
26
|
-
continue;
|
|
27
|
-
}
|
|
28
|
-
if (existing.status === 'done' || existing.status === 'in_progress') {
|
|
29
|
-
result.kept.push(existing.folder);
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
if (existing.status === 'cancelled') {
|
|
33
|
-
this.deleteTask(featureName, existing.folder);
|
|
34
|
-
result.removed.push(existing.folder);
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
const stillInPlan = planTasks.some(p => p.folder === existing.folder);
|
|
38
|
-
if (!stillInPlan) {
|
|
39
|
-
this.deleteTask(featureName, existing.folder);
|
|
40
|
-
result.removed.push(existing.folder);
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
result.kept.push(existing.folder);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
for (const planTask of planTasks) {
|
|
47
|
-
if (!existingByName.has(planTask.folder)) {
|
|
48
|
-
this.createFromPlan(featureName, planTask, planTasks);
|
|
49
|
-
result.created.push(planTask.folder);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return result;
|
|
53
|
-
}
|
|
54
|
-
create(featureName, name, order) {
|
|
55
|
-
const tasksPath = getTasksPath(this.projectRoot, featureName);
|
|
56
|
-
const existingFolders = this.listFolders(featureName);
|
|
57
|
-
const nextOrder = order ?? this.getNextOrder(existingFolders);
|
|
58
|
-
const folder = `${String(nextOrder).padStart(2, '0')}-${name}`;
|
|
59
|
-
const taskPath = getTaskPath(this.projectRoot, featureName, folder);
|
|
60
|
-
ensureDir(taskPath);
|
|
61
|
-
const status = {
|
|
62
|
-
status: 'pending',
|
|
63
|
-
origin: 'manual',
|
|
64
|
-
};
|
|
65
|
-
writeJson(getTaskStatusPath(this.projectRoot, featureName, folder), status);
|
|
66
|
-
return folder;
|
|
67
|
-
}
|
|
68
|
-
createFromPlan(featureName, task, allTasks) {
|
|
69
|
-
const taskPath = getTaskPath(this.projectRoot, featureName, task.folder);
|
|
70
|
-
ensureDir(taskPath);
|
|
71
|
-
const status = {
|
|
72
|
-
status: 'pending',
|
|
73
|
-
origin: 'plan',
|
|
74
|
-
};
|
|
75
|
-
writeJson(getTaskStatusPath(this.projectRoot, featureName, task.folder), status);
|
|
76
|
-
// Write enhanced spec.md with full context
|
|
77
|
-
const specLines = [
|
|
78
|
-
`# Task ${task.order}: ${task.name}`,
|
|
79
|
-
'',
|
|
80
|
-
`**Feature:** ${featureName}`,
|
|
81
|
-
`**Folder:** ${task.folder}`,
|
|
82
|
-
`**Status:** pending`,
|
|
83
|
-
'',
|
|
84
|
-
'---',
|
|
85
|
-
'',
|
|
86
|
-
'## Description',
|
|
87
|
-
'',
|
|
88
|
-
task.description || '_No description provided in plan_',
|
|
89
|
-
'',
|
|
90
|
-
];
|
|
91
|
-
// Add prior tasks section if not first task
|
|
92
|
-
if (task.order > 1) {
|
|
93
|
-
const priorTasks = allTasks.filter(t => t.order < task.order);
|
|
94
|
-
if (priorTasks.length > 0) {
|
|
95
|
-
specLines.push('---', '', '## Prior Tasks', '');
|
|
96
|
-
for (const prior of priorTasks) {
|
|
97
|
-
specLines.push(`- **${prior.order}. ${prior.name}** (${prior.folder})`);
|
|
98
|
-
}
|
|
99
|
-
specLines.push('');
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
// Add next tasks section if not last task
|
|
103
|
-
const nextTasks = allTasks.filter(t => t.order > task.order);
|
|
104
|
-
if (nextTasks.length > 0) {
|
|
105
|
-
specLines.push('---', '', '## Upcoming Tasks', '');
|
|
106
|
-
for (const next of nextTasks) {
|
|
107
|
-
specLines.push(`- **${next.order}. ${next.name}** (${next.folder})`);
|
|
108
|
-
}
|
|
109
|
-
specLines.push('');
|
|
110
|
-
}
|
|
111
|
-
writeText(getTaskSpecPath(this.projectRoot, featureName, task.folder), specLines.join('\n'));
|
|
112
|
-
}
|
|
113
|
-
writeSpec(featureName, taskFolder, content) {
|
|
114
|
-
const specPath = getTaskSpecPath(this.projectRoot, featureName, taskFolder);
|
|
115
|
-
writeText(specPath, content);
|
|
116
|
-
return specPath;
|
|
117
|
-
}
|
|
118
|
-
update(featureName, taskFolder, updates) {
|
|
119
|
-
const statusPath = getTaskStatusPath(this.projectRoot, featureName, taskFolder);
|
|
120
|
-
const current = readJson(statusPath);
|
|
121
|
-
if (!current) {
|
|
122
|
-
throw new Error(`Task '${taskFolder}' not found`);
|
|
123
|
-
}
|
|
124
|
-
const updated = {
|
|
125
|
-
...current,
|
|
126
|
-
...updates,
|
|
127
|
-
};
|
|
128
|
-
if (updates.status === 'in_progress' && !current.startedAt) {
|
|
129
|
-
updated.startedAt = new Date().toISOString();
|
|
130
|
-
}
|
|
131
|
-
if (updates.status === 'done' && !current.completedAt) {
|
|
132
|
-
updated.completedAt = new Date().toISOString();
|
|
133
|
-
}
|
|
134
|
-
writeJson(statusPath, updated);
|
|
135
|
-
return updated;
|
|
136
|
-
}
|
|
137
|
-
get(featureName, taskFolder) {
|
|
138
|
-
const statusPath = getTaskStatusPath(this.projectRoot, featureName, taskFolder);
|
|
139
|
-
const status = readJson(statusPath);
|
|
140
|
-
if (!status)
|
|
141
|
-
return null;
|
|
142
|
-
return {
|
|
143
|
-
folder: taskFolder,
|
|
144
|
-
name: taskFolder.replace(/^\d+-/, ''),
|
|
145
|
-
status: status.status,
|
|
146
|
-
origin: status.origin,
|
|
147
|
-
summary: status.summary,
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
list(featureName) {
|
|
151
|
-
const folders = this.listFolders(featureName);
|
|
152
|
-
return folders
|
|
153
|
-
.map(folder => this.get(featureName, folder))
|
|
154
|
-
.filter((t) => t !== null);
|
|
155
|
-
}
|
|
156
|
-
writeReport(featureName, taskFolder, report) {
|
|
157
|
-
const reportPath = getTaskReportPath(this.projectRoot, featureName, taskFolder);
|
|
158
|
-
writeText(reportPath, report);
|
|
159
|
-
return reportPath;
|
|
160
|
-
}
|
|
161
|
-
listFolders(featureName) {
|
|
162
|
-
const tasksPath = getTasksPath(this.projectRoot, featureName);
|
|
163
|
-
if (!fileExists(tasksPath))
|
|
164
|
-
return [];
|
|
165
|
-
return fs.readdirSync(tasksPath, { withFileTypes: true })
|
|
166
|
-
.filter(d => d.isDirectory())
|
|
167
|
-
.map(d => d.name)
|
|
168
|
-
.sort();
|
|
169
|
-
}
|
|
170
|
-
deleteTask(featureName, taskFolder) {
|
|
171
|
-
const taskPath = getTaskPath(this.projectRoot, featureName, taskFolder);
|
|
172
|
-
if (fileExists(taskPath)) {
|
|
173
|
-
fs.rmSync(taskPath, { recursive: true });
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
getNextOrder(existingFolders) {
|
|
177
|
-
if (existingFolders.length === 0)
|
|
178
|
-
return 1;
|
|
179
|
-
const orders = existingFolders
|
|
180
|
-
.map(f => parseInt(f.split('-')[0], 10))
|
|
181
|
-
.filter(n => !isNaN(n));
|
|
182
|
-
return Math.max(...orders, 0) + 1;
|
|
183
|
-
}
|
|
184
|
-
parseTasksFromPlan(content) {
|
|
185
|
-
const tasks = [];
|
|
186
|
-
const lines = content.split('\n');
|
|
187
|
-
let currentTask = null;
|
|
188
|
-
let descriptionLines = [];
|
|
189
|
-
for (const line of lines) {
|
|
190
|
-
// Check for task header: ### N. Task Name
|
|
191
|
-
const taskMatch = line.match(/^###\s+(\d+)\.\s+(.+)$/);
|
|
192
|
-
if (taskMatch) {
|
|
193
|
-
// Save previous task if exists
|
|
194
|
-
if (currentTask) {
|
|
195
|
-
currentTask.description = descriptionLines.join('\n').trim();
|
|
196
|
-
tasks.push(currentTask);
|
|
197
|
-
}
|
|
198
|
-
const order = parseInt(taskMatch[1], 10);
|
|
199
|
-
const rawName = taskMatch[2].trim();
|
|
200
|
-
const folderName = rawName.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
|
|
201
|
-
const folder = `${String(order).padStart(2, '0')}-${folderName}`;
|
|
202
|
-
currentTask = {
|
|
203
|
-
folder,
|
|
204
|
-
order,
|
|
205
|
-
name: rawName,
|
|
206
|
-
description: '',
|
|
207
|
-
};
|
|
208
|
-
descriptionLines = [];
|
|
209
|
-
}
|
|
210
|
-
else if (currentTask) {
|
|
211
|
-
// Check for end of task section (next ## header or ### without number)
|
|
212
|
-
if (line.match(/^##\s+/) || line.match(/^###\s+[^0-9]/)) {
|
|
213
|
-
currentTask.description = descriptionLines.join('\n').trim();
|
|
214
|
-
tasks.push(currentTask);
|
|
215
|
-
currentTask = null;
|
|
216
|
-
descriptionLines = [];
|
|
217
|
-
}
|
|
218
|
-
else {
|
|
219
|
-
descriptionLines.push(line);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
// Don't forget the last task
|
|
224
|
-
if (currentTask) {
|
|
225
|
-
currentTask.description = descriptionLines.join('\n').trim();
|
|
226
|
-
tasks.push(currentTask);
|
|
227
|
-
}
|
|
228
|
-
return tasks;
|
|
229
|
-
}
|
|
230
|
-
createSubtask(featureName, taskFolder, name, type) {
|
|
231
|
-
const subtasksPath = getSubtasksPath(this.projectRoot, featureName, taskFolder);
|
|
232
|
-
ensureDir(subtasksPath);
|
|
233
|
-
const existingFolders = this.listSubtaskFolders(featureName, taskFolder);
|
|
234
|
-
const taskOrder = parseInt(taskFolder.split('-')[0], 10);
|
|
235
|
-
const nextOrder = existingFolders.length + 1;
|
|
236
|
-
const subtaskId = `${taskOrder}.${nextOrder}`;
|
|
237
|
-
const folderName = `${nextOrder}-${this.slugify(name)}`;
|
|
238
|
-
const subtaskPath = getSubtaskPath(this.projectRoot, featureName, taskFolder, folderName);
|
|
239
|
-
ensureDir(subtaskPath);
|
|
240
|
-
const subtaskStatus = {
|
|
241
|
-
status: 'pending',
|
|
242
|
-
type,
|
|
243
|
-
createdAt: new Date().toISOString(),
|
|
244
|
-
};
|
|
245
|
-
writeJson(getSubtaskStatusPath(this.projectRoot, featureName, taskFolder, folderName), subtaskStatus);
|
|
246
|
-
const specContent = `# Subtask: ${name}\n\n**Type:** ${type || 'custom'}\n**ID:** ${subtaskId}\n\n## Instructions\n\n_Add detailed instructions here_\n`;
|
|
247
|
-
writeText(getSubtaskSpecPath(this.projectRoot, featureName, taskFolder, folderName), specContent);
|
|
248
|
-
return {
|
|
249
|
-
id: subtaskId,
|
|
250
|
-
name,
|
|
251
|
-
folder: folderName,
|
|
252
|
-
status: 'pending',
|
|
253
|
-
type,
|
|
254
|
-
createdAt: subtaskStatus.createdAt,
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
updateSubtask(featureName, taskFolder, subtaskId, status) {
|
|
258
|
-
const subtaskFolder = this.findSubtaskFolder(featureName, taskFolder, subtaskId);
|
|
259
|
-
if (!subtaskFolder) {
|
|
260
|
-
throw new Error(`Subtask '${subtaskId}' not found in task '${taskFolder}'`);
|
|
261
|
-
}
|
|
262
|
-
const statusPath = getSubtaskStatusPath(this.projectRoot, featureName, taskFolder, subtaskFolder);
|
|
263
|
-
const current = readJson(statusPath);
|
|
264
|
-
if (!current) {
|
|
265
|
-
throw new Error(`Subtask status not found for '${subtaskId}'`);
|
|
266
|
-
}
|
|
267
|
-
const updated = { ...current, status };
|
|
268
|
-
if (status === 'done' && !current.completedAt) {
|
|
269
|
-
updated.completedAt = new Date().toISOString();
|
|
270
|
-
}
|
|
271
|
-
writeJson(statusPath, updated);
|
|
272
|
-
const name = subtaskFolder.replace(/^\d+-/, '');
|
|
273
|
-
return {
|
|
274
|
-
id: subtaskId,
|
|
275
|
-
name,
|
|
276
|
-
folder: subtaskFolder,
|
|
277
|
-
status,
|
|
278
|
-
type: current.type,
|
|
279
|
-
createdAt: current.createdAt,
|
|
280
|
-
completedAt: updated.completedAt,
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
listSubtasks(featureName, taskFolder) {
|
|
284
|
-
const folders = this.listSubtaskFolders(featureName, taskFolder);
|
|
285
|
-
const taskOrder = parseInt(taskFolder.split('-')[0], 10);
|
|
286
|
-
return folders.map((folder, index) => {
|
|
287
|
-
const statusPath = getSubtaskStatusPath(this.projectRoot, featureName, taskFolder, folder);
|
|
288
|
-
const status = readJson(statusPath);
|
|
289
|
-
const name = folder.replace(/^\d+-/, '');
|
|
290
|
-
const subtaskOrder = parseInt(folder.split('-')[0], 10) || (index + 1);
|
|
291
|
-
return {
|
|
292
|
-
id: `${taskOrder}.${subtaskOrder}`,
|
|
293
|
-
name,
|
|
294
|
-
folder,
|
|
295
|
-
status: status?.status || 'pending',
|
|
296
|
-
type: status?.type,
|
|
297
|
-
createdAt: status?.createdAt,
|
|
298
|
-
completedAt: status?.completedAt,
|
|
299
|
-
};
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
deleteSubtask(featureName, taskFolder, subtaskId) {
|
|
303
|
-
const subtaskFolder = this.findSubtaskFolder(featureName, taskFolder, subtaskId);
|
|
304
|
-
if (!subtaskFolder) {
|
|
305
|
-
throw new Error(`Subtask '${subtaskId}' not found in task '${taskFolder}'`);
|
|
306
|
-
}
|
|
307
|
-
const subtaskPath = getSubtaskPath(this.projectRoot, featureName, taskFolder, subtaskFolder);
|
|
308
|
-
if (fileExists(subtaskPath)) {
|
|
309
|
-
fs.rmSync(subtaskPath, { recursive: true });
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
getSubtask(featureName, taskFolder, subtaskId) {
|
|
313
|
-
const subtaskFolder = this.findSubtaskFolder(featureName, taskFolder, subtaskId);
|
|
314
|
-
if (!subtaskFolder)
|
|
315
|
-
return null;
|
|
316
|
-
const statusPath = getSubtaskStatusPath(this.projectRoot, featureName, taskFolder, subtaskFolder);
|
|
317
|
-
const status = readJson(statusPath);
|
|
318
|
-
if (!status)
|
|
319
|
-
return null;
|
|
320
|
-
const taskOrder = parseInt(taskFolder.split('-')[0], 10);
|
|
321
|
-
const subtaskOrder = parseInt(subtaskFolder.split('-')[0], 10);
|
|
322
|
-
const name = subtaskFolder.replace(/^\d+-/, '');
|
|
323
|
-
return {
|
|
324
|
-
id: `${taskOrder}.${subtaskOrder}`,
|
|
325
|
-
name,
|
|
326
|
-
folder: subtaskFolder,
|
|
327
|
-
status: status.status,
|
|
328
|
-
type: status.type,
|
|
329
|
-
createdAt: status.createdAt,
|
|
330
|
-
completedAt: status.completedAt,
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
writeSubtaskSpec(featureName, taskFolder, subtaskId, content) {
|
|
334
|
-
const subtaskFolder = this.findSubtaskFolder(featureName, taskFolder, subtaskId);
|
|
335
|
-
if (!subtaskFolder) {
|
|
336
|
-
throw new Error(`Subtask '${subtaskId}' not found in task '${taskFolder}'`);
|
|
337
|
-
}
|
|
338
|
-
const specPath = getSubtaskSpecPath(this.projectRoot, featureName, taskFolder, subtaskFolder);
|
|
339
|
-
writeText(specPath, content);
|
|
340
|
-
return specPath;
|
|
341
|
-
}
|
|
342
|
-
writeSubtaskReport(featureName, taskFolder, subtaskId, content) {
|
|
343
|
-
const subtaskFolder = this.findSubtaskFolder(featureName, taskFolder, subtaskId);
|
|
344
|
-
if (!subtaskFolder) {
|
|
345
|
-
throw new Error(`Subtask '${subtaskId}' not found in task '${taskFolder}'`);
|
|
346
|
-
}
|
|
347
|
-
const reportPath = getSubtaskReportPath(this.projectRoot, featureName, taskFolder, subtaskFolder);
|
|
348
|
-
writeText(reportPath, content);
|
|
349
|
-
return reportPath;
|
|
350
|
-
}
|
|
351
|
-
readSubtaskSpec(featureName, taskFolder, subtaskId) {
|
|
352
|
-
const subtaskFolder = this.findSubtaskFolder(featureName, taskFolder, subtaskId);
|
|
353
|
-
if (!subtaskFolder)
|
|
354
|
-
return null;
|
|
355
|
-
const specPath = getSubtaskSpecPath(this.projectRoot, featureName, taskFolder, subtaskFolder);
|
|
356
|
-
return readText(specPath);
|
|
357
|
-
}
|
|
358
|
-
readSubtaskReport(featureName, taskFolder, subtaskId) {
|
|
359
|
-
const subtaskFolder = this.findSubtaskFolder(featureName, taskFolder, subtaskId);
|
|
360
|
-
if (!subtaskFolder)
|
|
361
|
-
return null;
|
|
362
|
-
const reportPath = getSubtaskReportPath(this.projectRoot, featureName, taskFolder, subtaskFolder);
|
|
363
|
-
return readText(reportPath);
|
|
364
|
-
}
|
|
365
|
-
listSubtaskFolders(featureName, taskFolder) {
|
|
366
|
-
const subtasksPath = getSubtasksPath(this.projectRoot, featureName, taskFolder);
|
|
367
|
-
if (!fileExists(subtasksPath))
|
|
368
|
-
return [];
|
|
369
|
-
return fs.readdirSync(subtasksPath, { withFileTypes: true })
|
|
370
|
-
.filter(d => d.isDirectory())
|
|
371
|
-
.map(d => d.name)
|
|
372
|
-
.sort();
|
|
373
|
-
}
|
|
374
|
-
findSubtaskFolder(featureName, taskFolder, subtaskId) {
|
|
375
|
-
const folders = this.listSubtaskFolders(featureName, taskFolder);
|
|
376
|
-
const subtaskOrder = subtaskId.split('.')[1];
|
|
377
|
-
return folders.find(f => f.startsWith(`${subtaskOrder}-`)) || null;
|
|
378
|
-
}
|
|
379
|
-
slugify(name) {
|
|
380
|
-
return name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
|
|
381
|
-
}
|
|
382
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
-
import * as fs from "fs";
|
|
3
|
-
import { TaskService } from "./taskService";
|
|
4
|
-
import { FeatureService } from "./featureService";
|
|
5
|
-
import { PlanService } from "./planService";
|
|
6
|
-
import { getTaskPath, getTaskStatusPath, getTaskReportPath, getSubtaskPath, getSubtaskStatusPath, getSubtaskSpecPath } from "../utils/paths";
|
|
7
|
-
const TEST_ROOT = "/tmp/hive-test-task";
|
|
8
|
-
describe("TaskService", () => {
|
|
9
|
-
let taskService;
|
|
10
|
-
let featureService;
|
|
11
|
-
let planService;
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
fs.rmSync(TEST_ROOT, { recursive: true, force: true });
|
|
14
|
-
fs.mkdirSync(TEST_ROOT, { recursive: true });
|
|
15
|
-
featureService = new FeatureService(TEST_ROOT);
|
|
16
|
-
planService = new PlanService(TEST_ROOT);
|
|
17
|
-
taskService = new TaskService(TEST_ROOT);
|
|
18
|
-
featureService.create("test-feature");
|
|
19
|
-
});
|
|
20
|
-
afterEach(() => {
|
|
21
|
-
fs.rmSync(TEST_ROOT, { recursive: true, force: true });
|
|
22
|
-
});
|
|
23
|
-
describe("sync", () => {
|
|
24
|
-
it("throws when no plan exists", () => {
|
|
25
|
-
featureService.create("no-plan");
|
|
26
|
-
expect(() => taskService.sync("no-plan")).toThrow();
|
|
27
|
-
});
|
|
28
|
-
it("creates tasks from plan", () => {
|
|
29
|
-
planService.write("test-feature", `# Plan
|
|
30
|
-
|
|
31
|
-
## Tasks
|
|
32
|
-
|
|
33
|
-
### 1. Setup Database
|
|
34
|
-
Description
|
|
35
|
-
|
|
36
|
-
### 2. Create API
|
|
37
|
-
Description
|
|
38
|
-
`);
|
|
39
|
-
const result = taskService.sync("test-feature");
|
|
40
|
-
expect(result.created).toContain("01-setup-database");
|
|
41
|
-
expect(result.created).toContain("02-create-api");
|
|
42
|
-
expect(result.created.length).toBe(2);
|
|
43
|
-
});
|
|
44
|
-
it("keeps done tasks even if removed from plan", () => {
|
|
45
|
-
planService.write("test-feature", `# Plan\n\n## Tasks\n\n### 1. First Task\nDesc`);
|
|
46
|
-
taskService.sync("test-feature");
|
|
47
|
-
taskService.update("test-feature", "01-first-task", { status: "done" });
|
|
48
|
-
planService.write("test-feature", `# Plan\n\n## Tasks\n\n### 1. Different Task\nDesc`);
|
|
49
|
-
const result = taskService.sync("test-feature");
|
|
50
|
-
expect(result.kept).toContain("01-first-task");
|
|
51
|
-
});
|
|
52
|
-
it("removes cancelled tasks", () => {
|
|
53
|
-
planService.write("test-feature", `# Plan\n\n## Tasks\n\n### 1. Task One\nDesc`);
|
|
54
|
-
taskService.sync("test-feature");
|
|
55
|
-
taskService.update("test-feature", "01-task-one", { status: "cancelled" });
|
|
56
|
-
const result = taskService.sync("test-feature");
|
|
57
|
-
expect(result.removed).toContain("01-task-one");
|
|
58
|
-
});
|
|
59
|
-
it("preserves manual tasks", () => {
|
|
60
|
-
planService.write("test-feature", `# Plan\n\n## Tasks\n\n### 1. Plan Task\nDesc`);
|
|
61
|
-
taskService.sync("test-feature");
|
|
62
|
-
taskService.create("test-feature", "manual-task");
|
|
63
|
-
const result = taskService.sync("test-feature");
|
|
64
|
-
expect(result.manual).toContain("02-manual-task");
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
describe("create", () => {
|
|
68
|
-
it("creates a manual task", () => {
|
|
69
|
-
const folder = taskService.create("test-feature", "my-task");
|
|
70
|
-
expect(folder).toBe("01-my-task");
|
|
71
|
-
expect(fs.existsSync(getTaskPath(TEST_ROOT, "test-feature", folder))).toBe(true);
|
|
72
|
-
});
|
|
73
|
-
it("auto-increments order", () => {
|
|
74
|
-
taskService.create("test-feature", "first");
|
|
75
|
-
taskService.create("test-feature", "second");
|
|
76
|
-
const third = taskService.create("test-feature", "third");
|
|
77
|
-
expect(third).toBe("03-third");
|
|
78
|
-
});
|
|
79
|
-
it("respects explicit order", () => {
|
|
80
|
-
const folder = taskService.create("test-feature", "specific", 10);
|
|
81
|
-
expect(folder).toBe("10-specific");
|
|
82
|
-
});
|
|
83
|
-
it("creates task with pending status and manual origin", () => {
|
|
84
|
-
const folder = taskService.create("test-feature", "test");
|
|
85
|
-
const task = taskService.get("test-feature", folder);
|
|
86
|
-
expect(task?.status).toBe("pending");
|
|
87
|
-
expect(task?.origin).toBe("manual");
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
describe("update", () => {
|
|
91
|
-
it("updates task status", () => {
|
|
92
|
-
const folder = taskService.create("test-feature", "task");
|
|
93
|
-
taskService.update("test-feature", folder, { status: "in_progress" });
|
|
94
|
-
const task = taskService.get("test-feature", folder);
|
|
95
|
-
expect(task?.status).toBe("in_progress");
|
|
96
|
-
});
|
|
97
|
-
it("sets startedAt when status becomes in_progress", () => {
|
|
98
|
-
const folder = taskService.create("test-feature", "task");
|
|
99
|
-
taskService.update("test-feature", folder, { status: "in_progress" });
|
|
100
|
-
const statusPath = getTaskStatusPath(TEST_ROOT, "test-feature", folder);
|
|
101
|
-
const status = JSON.parse(fs.readFileSync(statusPath, "utf-8"));
|
|
102
|
-
expect(status.startedAt).toBeDefined();
|
|
103
|
-
});
|
|
104
|
-
it("sets completedAt when status becomes done", () => {
|
|
105
|
-
const folder = taskService.create("test-feature", "task");
|
|
106
|
-
taskService.update("test-feature", folder, { status: "done" });
|
|
107
|
-
const statusPath = getTaskStatusPath(TEST_ROOT, "test-feature", folder);
|
|
108
|
-
const status = JSON.parse(fs.readFileSync(statusPath, "utf-8"));
|
|
109
|
-
expect(status.completedAt).toBeDefined();
|
|
110
|
-
});
|
|
111
|
-
it("updates summary", () => {
|
|
112
|
-
const folder = taskService.create("test-feature", "task");
|
|
113
|
-
taskService.update("test-feature", folder, { summary: "Completed setup" });
|
|
114
|
-
const task = taskService.get("test-feature", folder);
|
|
115
|
-
expect(task?.summary).toBe("Completed setup");
|
|
116
|
-
});
|
|
117
|
-
it("throws for non-existing task", () => {
|
|
118
|
-
expect(() => taskService.update("test-feature", "nope", { status: "done" })).toThrow();
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
describe("get", () => {
|
|
122
|
-
it("returns task info", () => {
|
|
123
|
-
const folder = taskService.create("test-feature", "my-task");
|
|
124
|
-
const task = taskService.get("test-feature", folder);
|
|
125
|
-
expect(task).not.toBeNull();
|
|
126
|
-
expect(task.folder).toBe("01-my-task");
|
|
127
|
-
expect(task.name).toBe("my-task");
|
|
128
|
-
expect(task.status).toBe("pending");
|
|
129
|
-
expect(task.origin).toBe("manual");
|
|
130
|
-
});
|
|
131
|
-
it("returns null for non-existing task", () => {
|
|
132
|
-
expect(taskService.get("test-feature", "nope")).toBeNull();
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
describe("list", () => {
|
|
136
|
-
it("returns empty array when no tasks", () => {
|
|
137
|
-
expect(taskService.list("test-feature")).toEqual([]);
|
|
138
|
-
});
|
|
139
|
-
it("returns all tasks sorted", () => {
|
|
140
|
-
taskService.create("test-feature", "third", 3);
|
|
141
|
-
taskService.create("test-feature", "first", 1);
|
|
142
|
-
taskService.create("test-feature", "second", 2);
|
|
143
|
-
const tasks = taskService.list("test-feature");
|
|
144
|
-
expect(tasks.length).toBe(3);
|
|
145
|
-
expect(tasks[0].folder).toBe("01-first");
|
|
146
|
-
expect(tasks[1].folder).toBe("02-second");
|
|
147
|
-
expect(tasks[2].folder).toBe("03-third");
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
describe("writeReport", () => {
|
|
151
|
-
it("writes report file", () => {
|
|
152
|
-
const folder = taskService.create("test-feature", "task");
|
|
153
|
-
const report = "## Summary\n\nCompleted the task successfully.";
|
|
154
|
-
taskService.writeReport("test-feature", folder, report);
|
|
155
|
-
const reportPath = getTaskReportPath(TEST_ROOT, "test-feature", folder);
|
|
156
|
-
expect(fs.readFileSync(reportPath, "utf-8")).toBe(report);
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
describe("subtasks", () => {
|
|
160
|
-
let taskFolder;
|
|
161
|
-
beforeEach(() => {
|
|
162
|
-
taskFolder = taskService.create("test-feature", "parent-task");
|
|
163
|
-
});
|
|
164
|
-
describe("createSubtask", () => {
|
|
165
|
-
it("creates subtask folder with status.json and spec.md", () => {
|
|
166
|
-
const subtask = taskService.createSubtask("test-feature", taskFolder, "Write tests", "test");
|
|
167
|
-
expect(subtask.id).toBe("1.1");
|
|
168
|
-
expect(subtask.name).toBe("Write tests");
|
|
169
|
-
expect(subtask.folder).toBe("1-write-tests");
|
|
170
|
-
expect(subtask.status).toBe("pending");
|
|
171
|
-
expect(subtask.type).toBe("test");
|
|
172
|
-
const subtaskPath = getSubtaskPath(TEST_ROOT, "test-feature", taskFolder, subtask.folder);
|
|
173
|
-
expect(fs.existsSync(subtaskPath)).toBe(true);
|
|
174
|
-
const statusPath = getSubtaskStatusPath(TEST_ROOT, "test-feature", taskFolder, subtask.folder);
|
|
175
|
-
expect(fs.existsSync(statusPath)).toBe(true);
|
|
176
|
-
const specPath = getSubtaskSpecPath(TEST_ROOT, "test-feature", taskFolder, subtask.folder);
|
|
177
|
-
expect(fs.existsSync(specPath)).toBe(true);
|
|
178
|
-
expect(fs.readFileSync(specPath, "utf-8")).toContain("Write tests");
|
|
179
|
-
});
|
|
180
|
-
it("auto-increments subtask order", () => {
|
|
181
|
-
const first = taskService.createSubtask("test-feature", taskFolder, "First", "test");
|
|
182
|
-
const second = taskService.createSubtask("test-feature", taskFolder, "Second", "implement");
|
|
183
|
-
const third = taskService.createSubtask("test-feature", taskFolder, "Third", "verify");
|
|
184
|
-
expect(first.id).toBe("1.1");
|
|
185
|
-
expect(second.id).toBe("1.2");
|
|
186
|
-
expect(third.id).toBe("1.3");
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
describe("listSubtasks", () => {
|
|
190
|
-
it("returns empty array when no subtasks", () => {
|
|
191
|
-
expect(taskService.listSubtasks("test-feature", taskFolder)).toEqual([]);
|
|
192
|
-
});
|
|
193
|
-
it("returns all subtasks sorted by order", () => {
|
|
194
|
-
taskService.createSubtask("test-feature", taskFolder, "Third", "verify");
|
|
195
|
-
taskService.createSubtask("test-feature", taskFolder, "First", "test");
|
|
196
|
-
const subtasks = taskService.listSubtasks("test-feature", taskFolder);
|
|
197
|
-
expect(subtasks.length).toBe(2);
|
|
198
|
-
expect(subtasks[0].folder).toBe("1-third");
|
|
199
|
-
expect(subtasks[1].folder).toBe("2-first");
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
describe("updateSubtask", () => {
|
|
203
|
-
it("updates subtask status", () => {
|
|
204
|
-
const subtask = taskService.createSubtask("test-feature", taskFolder, "Test", "test");
|
|
205
|
-
const updated = taskService.updateSubtask("test-feature", taskFolder, subtask.id, "in_progress");
|
|
206
|
-
expect(updated.status).toBe("in_progress");
|
|
207
|
-
});
|
|
208
|
-
it("sets completedAt when status becomes done", () => {
|
|
209
|
-
const subtask = taskService.createSubtask("test-feature", taskFolder, "Test", "test");
|
|
210
|
-
const updated = taskService.updateSubtask("test-feature", taskFolder, subtask.id, "done");
|
|
211
|
-
expect(updated.completedAt).toBeDefined();
|
|
212
|
-
});
|
|
213
|
-
it("throws for non-existing subtask", () => {
|
|
214
|
-
expect(() => taskService.updateSubtask("test-feature", taskFolder, "1.99", "done")).toThrow();
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
describe("deleteSubtask", () => {
|
|
218
|
-
it("removes subtask folder", () => {
|
|
219
|
-
const subtask = taskService.createSubtask("test-feature", taskFolder, "ToDelete", "test");
|
|
220
|
-
const subtaskPath = getSubtaskPath(TEST_ROOT, "test-feature", taskFolder, subtask.folder);
|
|
221
|
-
expect(fs.existsSync(subtaskPath)).toBe(true);
|
|
222
|
-
taskService.deleteSubtask("test-feature", taskFolder, subtask.id);
|
|
223
|
-
expect(fs.existsSync(subtaskPath)).toBe(false);
|
|
224
|
-
});
|
|
225
|
-
it("throws for non-existing subtask", () => {
|
|
226
|
-
expect(() => taskService.deleteSubtask("test-feature", taskFolder, "1.99")).toThrow();
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
describe("getSubtask", () => {
|
|
230
|
-
it("returns subtask info", () => {
|
|
231
|
-
const created = taskService.createSubtask("test-feature", taskFolder, "MySubtask", "implement");
|
|
232
|
-
const subtask = taskService.getSubtask("test-feature", taskFolder, created.id);
|
|
233
|
-
expect(subtask).not.toBeNull();
|
|
234
|
-
expect(subtask.id).toBe("1.1");
|
|
235
|
-
expect(subtask.name).toBe("mysubtask");
|
|
236
|
-
expect(subtask.folder).toBe("1-mysubtask");
|
|
237
|
-
expect(subtask.type).toBe("implement");
|
|
238
|
-
});
|
|
239
|
-
it("returns null for non-existing subtask", () => {
|
|
240
|
-
expect(taskService.getSubtask("test-feature", taskFolder, "1.99")).toBeNull();
|
|
241
|
-
});
|
|
242
|
-
});
|
|
243
|
-
describe("writeSubtaskSpec", () => {
|
|
244
|
-
it("writes spec.md content", () => {
|
|
245
|
-
const subtask = taskService.createSubtask("test-feature", taskFolder, "Test", "test");
|
|
246
|
-
const content = "# Custom Spec\n\nDetailed instructions here.";
|
|
247
|
-
const specPath = taskService.writeSubtaskSpec("test-feature", taskFolder, subtask.id, content);
|
|
248
|
-
expect(fs.readFileSync(specPath, "utf-8")).toBe(content);
|
|
249
|
-
});
|
|
250
|
-
it("throws for non-existing subtask", () => {
|
|
251
|
-
expect(() => taskService.writeSubtaskSpec("test-feature", taskFolder, "1.99", "content")).toThrow();
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
describe("writeSubtaskReport", () => {
|
|
255
|
-
it("writes report.md content", () => {
|
|
256
|
-
const subtask = taskService.createSubtask("test-feature", taskFolder, "Test", "test");
|
|
257
|
-
const content = "# Report\n\nCompleted successfully.";
|
|
258
|
-
const reportPath = taskService.writeSubtaskReport("test-feature", taskFolder, subtask.id, content);
|
|
259
|
-
expect(fs.readFileSync(reportPath, "utf-8")).toBe(content);
|
|
260
|
-
});
|
|
261
|
-
it("throws for non-existing subtask", () => {
|
|
262
|
-
expect(() => taskService.writeSubtaskReport("test-feature", taskFolder, "1.99", "content")).toThrow();
|
|
263
|
-
});
|
|
264
|
-
});
|
|
265
|
-
describe("readSubtaskSpec", () => {
|
|
266
|
-
it("reads spec.md content", () => {
|
|
267
|
-
const subtask = taskService.createSubtask("test-feature", taskFolder, "Test", "test");
|
|
268
|
-
const content = "Custom spec content";
|
|
269
|
-
taskService.writeSubtaskSpec("test-feature", taskFolder, subtask.id, content);
|
|
270
|
-
const spec = taskService.readSubtaskSpec("test-feature", taskFolder, subtask.id);
|
|
271
|
-
expect(spec).toBe(content);
|
|
272
|
-
});
|
|
273
|
-
it("returns null for non-existing subtask", () => {
|
|
274
|
-
expect(taskService.readSubtaskSpec("test-feature", taskFolder, "1.99")).toBeNull();
|
|
275
|
-
});
|
|
276
|
-
});
|
|
277
|
-
describe("readSubtaskReport", () => {
|
|
278
|
-
it("reads report.md content", () => {
|
|
279
|
-
const subtask = taskService.createSubtask("test-feature", taskFolder, "Test", "test");
|
|
280
|
-
const content = "Report content";
|
|
281
|
-
taskService.writeSubtaskReport("test-feature", taskFolder, subtask.id, content);
|
|
282
|
-
const report = taskService.readSubtaskReport("test-feature", taskFolder, subtask.id);
|
|
283
|
-
expect(report).toBe(content);
|
|
284
|
-
});
|
|
285
|
-
it("returns null for non-existing subtask", () => {
|
|
286
|
-
expect(taskService.readSubtaskReport("test-feature", taskFolder, "1.99")).toBeNull();
|
|
287
|
-
});
|
|
288
|
-
});
|
|
289
|
-
});
|
|
290
|
-
});
|