opencode-hive 0.6.0 → 0.8.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/dist/index.js CHANGED
@@ -12,13 +12,14 @@ const HIVE_SYSTEM_PROMPT = `
12
12
 
13
13
  Plan-first development: Write plan → User reviews → Approve → Execute tasks
14
14
 
15
- ### Tools (17 total)
15
+ ### Tools (24 total)
16
16
 
17
17
  | Domain | Tools |
18
18
  |--------|-------|
19
19
  | Feature | hive_feature_create, hive_feature_list, hive_feature_complete |
20
20
  | Plan | hive_plan_write, hive_plan_read, hive_plan_approve |
21
21
  | Task | hive_tasks_sync, hive_task_create, hive_task_update |
22
+ | Subtask | hive_subtask_create, hive_subtask_update, hive_subtask_list, hive_subtask_spec_write, hive_subtask_report_write |
22
23
  | Exec | hive_exec_start, hive_exec_complete, hive_exec_abort |
23
24
  | Merge | hive_merge, hive_worktree_list |
24
25
  | Context | hive_context_write, hive_context_read, hive_context_list |
@@ -37,6 +38,21 @@ Plan-first development: Write plan → User reviews → Approve → Execute task
37
38
  **Important:** \`hive_exec_complete\` commits changes to task branch but does NOT merge.
38
39
  Use \`hive_merge\` to explicitly integrate changes. Worktrees persist until manually removed.
39
40
 
41
+ ### Subtasks & TDD
42
+
43
+ For complex tasks, break work into subtasks:
44
+
45
+ \`\`\`
46
+ hive_subtask_create(task, "Write failing tests", "test")
47
+ hive_subtask_create(task, "Implement until green", "implement")
48
+ hive_subtask_create(task, "Run test suite", "verify")
49
+ \`\`\`
50
+
51
+ Subtask types: test, implement, review, verify, research, debug, custom
52
+
53
+ **Test-Driven Development**: For implementation tasks, consider writing tests first.
54
+ Tests define "done" and provide feedback loops that improve quality.
55
+
40
56
  ### Plan Format
41
57
 
42
58
  \`\`\`markdown
@@ -529,6 +545,115 @@ const plugin = async (ctx) => {
529
545
  }).join('\n');
530
546
  },
531
547
  }),
548
+ hive_subtask_create: tool({
549
+ description: 'Create a subtask within a task. Use for TDD: create test/implement/verify subtasks.',
550
+ args: {
551
+ task: tool.schema.string().describe('Task folder name'),
552
+ name: tool.schema.string().describe('Subtask description'),
553
+ type: tool.schema.enum(['test', 'implement', 'review', 'verify', 'research', 'debug', 'custom']).optional().describe('Subtask type'),
554
+ feature: tool.schema.string().optional().describe('Feature name (defaults to active)'),
555
+ },
556
+ async execute({ task, name, type, feature: explicitFeature }) {
557
+ const feature = resolveFeature(explicitFeature);
558
+ if (!feature)
559
+ return "Error: No feature specified. Create a feature or provide feature param.";
560
+ try {
561
+ const subtask = taskService.createSubtask(feature, task, name, type);
562
+ return `Subtask created: ${subtask.id} - ${subtask.name} [${subtask.type || 'custom'}]`;
563
+ }
564
+ catch (e) {
565
+ return `Error: ${e.message}`;
566
+ }
567
+ },
568
+ }),
569
+ hive_subtask_update: tool({
570
+ description: 'Update subtask status',
571
+ args: {
572
+ task: tool.schema.string().describe('Task folder name'),
573
+ subtask: tool.schema.string().describe('Subtask ID (e.g., "1.1")'),
574
+ status: tool.schema.enum(['pending', 'in_progress', 'done', 'cancelled']).describe('New status'),
575
+ feature: tool.schema.string().optional().describe('Feature name (defaults to active)'),
576
+ },
577
+ async execute({ task, subtask, status, feature: explicitFeature }) {
578
+ const feature = resolveFeature(explicitFeature);
579
+ if (!feature)
580
+ return "Error: No feature specified. Create a feature or provide feature param.";
581
+ try {
582
+ const updated = taskService.updateSubtask(feature, task, subtask, status);
583
+ return `Subtask ${updated.id} updated: ${updated.status}`;
584
+ }
585
+ catch (e) {
586
+ return `Error: ${e.message}`;
587
+ }
588
+ },
589
+ }),
590
+ hive_subtask_list: tool({
591
+ description: 'List all subtasks for a task',
592
+ args: {
593
+ task: tool.schema.string().describe('Task folder name'),
594
+ feature: tool.schema.string().optional().describe('Feature name (defaults to active)'),
595
+ },
596
+ async execute({ task, feature: explicitFeature }) {
597
+ const feature = resolveFeature(explicitFeature);
598
+ if (!feature)
599
+ return "Error: No feature specified. Create a feature or provide feature param.";
600
+ try {
601
+ const subtasks = taskService.listSubtasks(feature, task);
602
+ if (subtasks.length === 0)
603
+ return "No subtasks for this task.";
604
+ return subtasks.map(s => {
605
+ const typeTag = s.type ? ` [${s.type}]` : '';
606
+ const statusIcon = s.status === 'done' ? '✓' : s.status === 'in_progress' ? '→' : '○';
607
+ return `${statusIcon} ${s.id}: ${s.name}${typeTag}`;
608
+ }).join('\n');
609
+ }
610
+ catch (e) {
611
+ return `Error: ${e.message}`;
612
+ }
613
+ },
614
+ }),
615
+ hive_subtask_spec_write: tool({
616
+ description: 'Write spec.md for a subtask (detailed instructions)',
617
+ args: {
618
+ task: tool.schema.string().describe('Task folder name'),
619
+ subtask: tool.schema.string().describe('Subtask ID (e.g., "1.1")'),
620
+ content: tool.schema.string().describe('Spec content (markdown)'),
621
+ feature: tool.schema.string().optional().describe('Feature name (defaults to active)'),
622
+ },
623
+ async execute({ task, subtask, content, feature: explicitFeature }) {
624
+ const feature = resolveFeature(explicitFeature);
625
+ if (!feature)
626
+ return "Error: No feature specified. Create a feature or provide feature param.";
627
+ try {
628
+ const specPath = taskService.writeSubtaskSpec(feature, task, subtask, content);
629
+ return `Subtask spec written: ${specPath}`;
630
+ }
631
+ catch (e) {
632
+ return `Error: ${e.message}`;
633
+ }
634
+ },
635
+ }),
636
+ hive_subtask_report_write: tool({
637
+ description: 'Write report.md for a subtask (what was done)',
638
+ args: {
639
+ task: tool.schema.string().describe('Task folder name'),
640
+ subtask: tool.schema.string().describe('Subtask ID (e.g., "1.1")'),
641
+ content: tool.schema.string().describe('Report content (markdown)'),
642
+ feature: tool.schema.string().optional().describe('Feature name (defaults to active)'),
643
+ },
644
+ async execute({ task, subtask, content, feature: explicitFeature }) {
645
+ const feature = resolveFeature(explicitFeature);
646
+ if (!feature)
647
+ return "Error: No feature specified. Create a feature or provide feature param.";
648
+ try {
649
+ const reportPath = taskService.writeSubtaskReport(feature, task, subtask, content);
650
+ return `Subtask report written: ${reportPath}`;
651
+ }
652
+ catch (e) {
653
+ return `Error: ${e.message}`;
654
+ }
655
+ },
656
+ }),
532
657
  },
533
658
  command: {
534
659
  hive: {
@@ -1,4 +1,4 @@
1
- import { TaskStatus, TasksSyncResult, TaskInfo } from '../types.js';
1
+ import { TaskStatus, TaskStatusType, TasksSyncResult, TaskInfo, Subtask, SubtaskType } from '../types.js';
2
2
  export declare class TaskService {
3
3
  private projectRoot;
4
4
  constructor(projectRoot: string);
@@ -14,4 +14,16 @@ export declare class TaskService {
14
14
  private deleteTask;
15
15
  private getNextOrder;
16
16
  private parseTasksFromPlan;
17
+ createSubtask(featureName: string, taskFolder: string, name: string, type?: SubtaskType): Subtask;
18
+ updateSubtask(featureName: string, taskFolder: string, subtaskId: string, status: TaskStatusType): Subtask;
19
+ listSubtasks(featureName: string, taskFolder: string): Subtask[];
20
+ deleteSubtask(featureName: string, taskFolder: string, subtaskId: string): void;
21
+ getSubtask(featureName: string, taskFolder: string, subtaskId: string): Subtask | null;
22
+ writeSubtaskSpec(featureName: string, taskFolder: string, subtaskId: string, content: string): string;
23
+ writeSubtaskReport(featureName: string, taskFolder: string, subtaskId: string, content: string): string;
24
+ readSubtaskSpec(featureName: string, taskFolder: string, subtaskId: string): string | null;
25
+ readSubtaskReport(featureName: string, taskFolder: string, subtaskId: string): string | null;
26
+ private listSubtaskFolders;
27
+ private findSubtaskFolder;
28
+ private slugify;
17
29
  }
@@ -1,5 +1,5 @@
1
1
  import * as fs from 'fs';
2
- import { getTasksPath, getTaskPath, getTaskStatusPath, getTaskReportPath, getTaskSpecPath, getPlanPath, ensureDir, readJson, writeJson, readText, writeText, fileExists, } from '../utils/paths.js';
2
+ import { getTasksPath, getTaskPath, getTaskStatusPath, getTaskReportPath, getTaskSpecPath, getSubtasksPath, getSubtaskPath, getSubtaskStatusPath, getSubtaskSpecPath, getSubtaskReportPath, getPlanPath, ensureDir, readJson, writeJson, readText, writeText, fileExists, } from '../utils/paths.js';
3
3
  export class TaskService {
4
4
  projectRoot;
5
5
  constructor(projectRoot) {
@@ -227,4 +227,156 @@ export class TaskService {
227
227
  }
228
228
  return tasks;
229
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
+ }
230
382
  }
@@ -3,7 +3,7 @@ import * as fs from "fs";
3
3
  import { TaskService } from "./taskService";
4
4
  import { FeatureService } from "./featureService";
5
5
  import { PlanService } from "./planService";
6
- import { getTaskPath, getTaskStatusPath, getTaskReportPath } from "../utils/paths";
6
+ import { getTaskPath, getTaskStatusPath, getTaskReportPath, getSubtaskPath, getSubtaskStatusPath, getSubtaskSpecPath } from "../utils/paths";
7
7
  const TEST_ROOT = "/tmp/hive-test-task";
8
8
  describe("TaskService", () => {
9
9
  let taskService;
@@ -156,4 +156,135 @@ Description
156
156
  expect(fs.readFileSync(reportPath, "utf-8")).toBe(report);
157
157
  });
158
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
+ });
159
290
  });
package/dist/types.d.ts CHANGED
@@ -10,6 +10,22 @@ export interface FeatureJson {
10
10
  }
11
11
  export type TaskStatusType = 'pending' | 'in_progress' | 'done' | 'cancelled';
12
12
  export type TaskOrigin = 'plan' | 'manual';
13
+ export type SubtaskType = 'test' | 'implement' | 'review' | 'verify' | 'research' | 'debug' | 'custom';
14
+ export interface Subtask {
15
+ id: string;
16
+ name: string;
17
+ folder: string;
18
+ status: TaskStatusType;
19
+ type?: SubtaskType;
20
+ createdAt?: string;
21
+ completedAt?: string;
22
+ }
23
+ export interface SubtaskStatus {
24
+ status: TaskStatusType;
25
+ type?: SubtaskType;
26
+ createdAt: string;
27
+ completedAt?: string;
28
+ }
13
29
  export interface TaskStatus {
14
30
  status: TaskStatusType;
15
31
  origin: TaskOrigin;
@@ -17,6 +33,7 @@ export interface TaskStatus {
17
33
  startedAt?: string;
18
34
  completedAt?: string;
19
35
  baseCommit?: string;
36
+ subtasks?: Subtask[];
20
37
  }
21
38
  export interface PlanComment {
22
39
  id: string;
@@ -10,6 +10,11 @@ export declare function getTaskPath(projectRoot: string, featureName: string, ta
10
10
  export declare function getTaskStatusPath(projectRoot: string, featureName: string, taskFolder: string): string;
11
11
  export declare function getTaskReportPath(projectRoot: string, featureName: string, taskFolder: string): string;
12
12
  export declare function getTaskSpecPath(projectRoot: string, featureName: string, taskFolder: string): string;
13
+ export declare function getSubtasksPath(projectRoot: string, featureName: string, taskFolder: string): string;
14
+ export declare function getSubtaskPath(projectRoot: string, featureName: string, taskFolder: string, subtaskFolder: string): string;
15
+ export declare function getSubtaskStatusPath(projectRoot: string, featureName: string, taskFolder: string, subtaskFolder: string): string;
16
+ export declare function getSubtaskSpecPath(projectRoot: string, featureName: string, taskFolder: string, subtaskFolder: string): string;
17
+ export declare function getSubtaskReportPath(projectRoot: string, featureName: string, taskFolder: string, subtaskFolder: string): string;
13
18
  export declare function ensureDir(dirPath: string): void;
14
19
  export declare function fileExists(filePath: string): boolean;
15
20
  export declare function readJson<T>(filePath: string): T | null;
@@ -45,6 +45,24 @@ export function getTaskReportPath(projectRoot, featureName, taskFolder) {
45
45
  export function getTaskSpecPath(projectRoot, featureName, taskFolder) {
46
46
  return path.join(getTaskPath(projectRoot, featureName, taskFolder), 'spec.md');
47
47
  }
48
+ // Subtask paths
49
+ const SUBTASKS_DIR = 'subtasks';
50
+ const SPEC_FILE = 'spec.md';
51
+ export function getSubtasksPath(projectRoot, featureName, taskFolder) {
52
+ return path.join(getTaskPath(projectRoot, featureName, taskFolder), SUBTASKS_DIR);
53
+ }
54
+ export function getSubtaskPath(projectRoot, featureName, taskFolder, subtaskFolder) {
55
+ return path.join(getSubtasksPath(projectRoot, featureName, taskFolder), subtaskFolder);
56
+ }
57
+ export function getSubtaskStatusPath(projectRoot, featureName, taskFolder, subtaskFolder) {
58
+ return path.join(getSubtaskPath(projectRoot, featureName, taskFolder, subtaskFolder), STATUS_FILE);
59
+ }
60
+ export function getSubtaskSpecPath(projectRoot, featureName, taskFolder, subtaskFolder) {
61
+ return path.join(getSubtaskPath(projectRoot, featureName, taskFolder, subtaskFolder), SPEC_FILE);
62
+ }
63
+ export function getSubtaskReportPath(projectRoot, featureName, taskFolder, subtaskFolder) {
64
+ return path.join(getSubtaskPath(projectRoot, featureName, taskFolder, subtaskFolder), REPORT_FILE);
65
+ }
48
66
  export function ensureDir(dirPath) {
49
67
  if (!fs.existsSync(dirPath)) {
50
68
  fs.mkdirSync(dirPath, { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-hive",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Agent Hive - from vibe coding to hive coding",
6
6
  "license": "MIT WITH Commons-Clause",