opencode-hive 0.4.2 → 0.5.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.
Files changed (43) hide show
  1. package/README.md +0 -2
  2. package/dist/e2e/opencode-runtime-smoke.test.d.ts +1 -0
  3. package/dist/e2e/opencode-runtime-smoke.test.js +243 -0
  4. package/dist/e2e/plugin-smoke.test.d.ts +1 -0
  5. package/dist/e2e/plugin-smoke.test.js +127 -0
  6. package/dist/index.js +273 -75
  7. package/dist/services/contextService.d.ts +15 -0
  8. package/dist/services/contextService.js +59 -0
  9. package/dist/services/featureService.d.ts +0 -2
  10. package/dist/services/featureService.js +1 -11
  11. package/dist/services/featureService.test.d.ts +1 -0
  12. package/dist/services/featureService.test.js +127 -0
  13. package/dist/services/planService.test.d.ts +1 -0
  14. package/dist/services/planService.test.js +115 -0
  15. package/dist/services/sessionService.d.ts +31 -0
  16. package/dist/services/sessionService.js +125 -0
  17. package/dist/services/taskService.d.ts +2 -1
  18. package/dist/services/taskService.js +87 -12
  19. package/dist/services/taskService.test.d.ts +1 -0
  20. package/dist/services/taskService.test.js +159 -0
  21. package/dist/services/worktreeService.js +42 -17
  22. package/dist/services/worktreeService.test.d.ts +1 -0
  23. package/dist/services/worktreeService.test.js +117 -0
  24. package/dist/tools/contextTools.d.ts +93 -0
  25. package/dist/tools/contextTools.js +83 -0
  26. package/dist/tools/execTools.d.ts +3 -9
  27. package/dist/tools/execTools.js +14 -12
  28. package/dist/tools/featureTools.d.ts +4 -48
  29. package/dist/tools/featureTools.js +11 -51
  30. package/dist/tools/planTools.d.ts +5 -15
  31. package/dist/tools/planTools.js +16 -16
  32. package/dist/tools/sessionTools.d.ts +35 -0
  33. package/dist/tools/sessionTools.js +95 -0
  34. package/dist/tools/taskTools.d.ts +6 -18
  35. package/dist/tools/taskTools.js +18 -19
  36. package/dist/types.d.ts +35 -0
  37. package/dist/utils/detection.d.ts +12 -0
  38. package/dist/utils/detection.js +73 -0
  39. package/dist/utils/paths.d.ts +1 -1
  40. package/dist/utils/paths.js +2 -3
  41. package/dist/utils/paths.test.d.ts +1 -0
  42. package/dist/utils/paths.test.js +100 -0
  43. package/package.json +1 -1
@@ -0,0 +1,127 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
2
+ import * as fs from "fs";
3
+ import { FeatureService } from "./featureService";
4
+ import { getFeatureJsonPath, getFeaturePath } from "../utils/paths";
5
+ const TEST_ROOT = "/tmp/hive-test-feature";
6
+ describe("FeatureService", () => {
7
+ let service;
8
+ beforeEach(() => {
9
+ fs.rmSync(TEST_ROOT, { recursive: true, force: true });
10
+ fs.mkdirSync(TEST_ROOT, { recursive: true });
11
+ service = new FeatureService(TEST_ROOT);
12
+ });
13
+ afterEach(() => {
14
+ fs.rmSync(TEST_ROOT, { recursive: true, force: true });
15
+ });
16
+ describe("create", () => {
17
+ it("creates a new feature with default status", () => {
18
+ const feature = service.create("test-feature");
19
+ expect(feature.name).toBe("test-feature");
20
+ expect(feature.status).toBe("planning");
21
+ expect(feature.createdAt).toBeDefined();
22
+ expect(feature.ticket).toBeUndefined();
23
+ });
24
+ it("creates a feature with ticket", () => {
25
+ const feature = service.create("test-feature", "JIRA-123");
26
+ expect(feature.ticket).toBe("JIRA-123");
27
+ });
28
+ it("creates feature directory structure", () => {
29
+ service.create("my-feature");
30
+ const featurePath = getFeaturePath(TEST_ROOT, "my-feature");
31
+ expect(fs.existsSync(featurePath)).toBe(true);
32
+ expect(fs.existsSync(getFeatureJsonPath(TEST_ROOT, "my-feature"))).toBe(true);
33
+ });
34
+ it("throws if feature already exists", () => {
35
+ service.create("existing");
36
+ expect(() => service.create("existing")).toThrow();
37
+ });
38
+ });
39
+ describe("get", () => {
40
+ it("returns feature data", () => {
41
+ service.create("test-feature", "TICKET-1");
42
+ const feature = service.get("test-feature");
43
+ expect(feature).not.toBeNull();
44
+ expect(feature.name).toBe("test-feature");
45
+ expect(feature.ticket).toBe("TICKET-1");
46
+ });
47
+ it("returns null for non-existing feature", () => {
48
+ expect(service.get("nope")).toBeNull();
49
+ });
50
+ });
51
+ describe("list", () => {
52
+ it("returns empty array when no features", () => {
53
+ expect(service.list()).toEqual([]);
54
+ });
55
+ it("returns all feature names", () => {
56
+ service.create("feature-a");
57
+ service.create("feature-b");
58
+ service.create("feature-c");
59
+ const features = service.list();
60
+ expect(features).toContain("feature-a");
61
+ expect(features).toContain("feature-b");
62
+ expect(features).toContain("feature-c");
63
+ expect(features.length).toBe(3);
64
+ });
65
+ });
66
+ describe("updateStatus", () => {
67
+ it("updates feature status", () => {
68
+ service.create("test");
69
+ service.updateStatus("test", "approved");
70
+ const feature = service.get("test");
71
+ expect(feature.status).toBe("approved");
72
+ });
73
+ it("sets approvedAt when status becomes approved", () => {
74
+ service.create("test");
75
+ service.updateStatus("test", "approved");
76
+ const feature = service.get("test");
77
+ expect(feature.approvedAt).toBeDefined();
78
+ });
79
+ it("sets completedAt when status becomes completed", () => {
80
+ service.create("test");
81
+ service.updateStatus("test", "completed");
82
+ const feature = service.get("test");
83
+ expect(feature.completedAt).toBeDefined();
84
+ });
85
+ });
86
+ describe("complete", () => {
87
+ it("marks feature as completed", () => {
88
+ service.create("to-complete");
89
+ service.complete("to-complete");
90
+ const feature = service.get("to-complete");
91
+ expect(feature.status).toBe("completed");
92
+ expect(feature.completedAt).toBeDefined();
93
+ });
94
+ });
95
+ describe("getInfo", () => {
96
+ it("returns null for non-existing feature", () => {
97
+ expect(service.getInfo("nope")).toBeNull();
98
+ });
99
+ it("returns feature info with tasks array", () => {
100
+ service.create("info-test");
101
+ const info = service.getInfo("info-test");
102
+ expect(info).not.toBeNull();
103
+ expect(info.name).toBe("info-test");
104
+ expect(info.status).toBe("planning");
105
+ expect(info.tasks).toEqual([]);
106
+ expect(info.hasPlan).toBe(false);
107
+ expect(info.commentCount).toBe(0);
108
+ });
109
+ });
110
+ describe("session management", () => {
111
+ it("setSession stores session ID", () => {
112
+ service.create("session-test");
113
+ service.setSession("session-test", "sess_12345");
114
+ const feature = service.get("session-test");
115
+ expect(feature.sessionId).toBe("sess_12345");
116
+ });
117
+ it("getSession retrieves session ID", () => {
118
+ service.create("session-test");
119
+ service.setSession("session-test", "sess_67890");
120
+ expect(service.getSession("session-test")).toBe("sess_67890");
121
+ });
122
+ it("getSession returns undefined when no session", () => {
123
+ service.create("no-session");
124
+ expect(service.getSession("no-session")).toBeUndefined();
125
+ });
126
+ });
127
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,115 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
2
+ import * as fs from "fs";
3
+ import { PlanService } from "./planService";
4
+ import { FeatureService } from "./featureService";
5
+ import { getPlanPath, getCommentsPath } from "../utils/paths";
6
+ const TEST_ROOT = "/tmp/hive-test-plan";
7
+ describe("PlanService", () => {
8
+ let planService;
9
+ let featureService;
10
+ beforeEach(() => {
11
+ fs.rmSync(TEST_ROOT, { recursive: true, force: true });
12
+ fs.mkdirSync(TEST_ROOT, { recursive: true });
13
+ featureService = new FeatureService(TEST_ROOT);
14
+ planService = new PlanService(TEST_ROOT);
15
+ featureService.create("test-feature");
16
+ });
17
+ afterEach(() => {
18
+ fs.rmSync(TEST_ROOT, { recursive: true, force: true });
19
+ });
20
+ describe("write", () => {
21
+ it("writes plan content to file", () => {
22
+ const content = "# My Plan\n\n## Tasks\n\n### 1. First Task";
23
+ planService.write("test-feature", content);
24
+ const planPath = getPlanPath(TEST_ROOT, "test-feature");
25
+ expect(fs.readFileSync(planPath, "utf-8")).toBe(content);
26
+ });
27
+ it("clears existing comments when writing", () => {
28
+ const commentsPath = getCommentsPath(TEST_ROOT, "test-feature");
29
+ fs.writeFileSync(commentsPath, JSON.stringify({
30
+ threads: [{ id: "1", line: 1, body: "test", author: "user", timestamp: new Date().toISOString() }]
31
+ }));
32
+ planService.write("test-feature", "# New Plan");
33
+ const comments = planService.getComments("test-feature");
34
+ expect(comments).toEqual([]);
35
+ });
36
+ });
37
+ describe("read", () => {
38
+ it("returns null when no plan exists", () => {
39
+ featureService.create("empty-feature");
40
+ const result = planService.read("empty-feature");
41
+ expect(result).toBeNull();
42
+ });
43
+ it("returns plan content and status", () => {
44
+ const content = "# Test Plan";
45
+ planService.write("test-feature", content);
46
+ const result = planService.read("test-feature");
47
+ expect(result).not.toBeNull();
48
+ expect(result.content).toBe(content);
49
+ expect(result.status).toBe("planning");
50
+ expect(result.comments).toEqual([]);
51
+ });
52
+ it("includes comments in result", () => {
53
+ planService.write("test-feature", "# Plan");
54
+ planService.addComment("test-feature", {
55
+ line: 1,
56
+ body: "Looks good!",
57
+ author: "reviewer",
58
+ });
59
+ const result = planService.read("test-feature");
60
+ expect(result.comments.length).toBe(1);
61
+ expect(result.comments[0].body).toBe("Looks good!");
62
+ });
63
+ });
64
+ describe("approve", () => {
65
+ it("sets feature status to approved", () => {
66
+ planService.write("test-feature", "# Plan");
67
+ planService.approve("test-feature");
68
+ const feature = featureService.get("test-feature");
69
+ expect(feature.status).toBe("approved");
70
+ });
71
+ it("read returns approved status", () => {
72
+ planService.write("test-feature", "# Plan");
73
+ planService.approve("test-feature");
74
+ const result = planService.read("test-feature");
75
+ expect(result.status).toBe("approved");
76
+ });
77
+ });
78
+ describe("comments", () => {
79
+ it("addComment adds a comment", () => {
80
+ planService.write("test-feature", "# Plan");
81
+ planService.addComment("test-feature", {
82
+ line: 5,
83
+ body: "What about error handling?",
84
+ author: "reviewer",
85
+ });
86
+ const comments = planService.getComments("test-feature");
87
+ expect(comments.length).toBe(1);
88
+ expect(comments[0].body).toBe("What about error handling?");
89
+ expect(comments[0].id).toBeDefined();
90
+ expect(comments[0].timestamp).toBeDefined();
91
+ });
92
+ it("getComments returns empty array when no comments", () => {
93
+ planService.write("test-feature", "# Plan");
94
+ expect(planService.getComments("test-feature")).toEqual([]);
95
+ });
96
+ it("clearComments removes all comments", () => {
97
+ planService.write("test-feature", "# Plan");
98
+ planService.addComment("test-feature", {
99
+ line: 1,
100
+ body: "Comment 1",
101
+ author: "a",
102
+ });
103
+ planService.addComment("test-feature", {
104
+ line: 2,
105
+ body: "Comment 2",
106
+ author: "b",
107
+ });
108
+ planService.clearComments("test-feature");
109
+ expect(planService.getComments("test-feature")).toEqual([]);
110
+ });
111
+ it("getComments returns empty array for non-existing feature", () => {
112
+ expect(planService.getComments("nope")).toEqual([]);
113
+ });
114
+ });
115
+ });
@@ -0,0 +1,31 @@
1
+ export interface SessionInfo {
2
+ sessionId: string;
3
+ taskFolder?: string;
4
+ startedAt: string;
5
+ lastActiveAt: string;
6
+ messageCount?: number;
7
+ }
8
+ export interface SessionsJson {
9
+ master?: string;
10
+ sessions: SessionInfo[];
11
+ }
12
+ export declare class SessionService {
13
+ private projectRoot;
14
+ constructor(projectRoot: string);
15
+ private getSessionsPath;
16
+ private getSessions;
17
+ private saveSessions;
18
+ track(featureName: string, sessionId: string, taskFolder?: string): SessionInfo;
19
+ setMaster(featureName: string, sessionId: string): void;
20
+ getMaster(featureName: string): string | undefined;
21
+ list(featureName: string): SessionInfo[];
22
+ get(featureName: string, sessionId: string): SessionInfo | undefined;
23
+ getByTask(featureName: string, taskFolder: string): SessionInfo | undefined;
24
+ remove(featureName: string, sessionId: string): boolean;
25
+ /**
26
+ * Find which feature a session belongs to by searching all features
27
+ */
28
+ findFeatureBySession(sessionId: string): string | null;
29
+ fork(featureName: string, fromSessionId?: string): SessionInfo;
30
+ fresh(featureName: string, title?: string): SessionInfo;
31
+ }
@@ -0,0 +1,125 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { getFeaturePath, ensureDir, readJson, writeJson } from '../utils/paths.js';
4
+ export class SessionService {
5
+ projectRoot;
6
+ constructor(projectRoot) {
7
+ this.projectRoot = projectRoot;
8
+ }
9
+ getSessionsPath(featureName) {
10
+ return path.join(getFeaturePath(this.projectRoot, featureName), 'sessions.json');
11
+ }
12
+ getSessions(featureName) {
13
+ const sessionsPath = this.getSessionsPath(featureName);
14
+ return readJson(sessionsPath) || { sessions: [] };
15
+ }
16
+ saveSessions(featureName, data) {
17
+ const sessionsPath = this.getSessionsPath(featureName);
18
+ ensureDir(path.dirname(sessionsPath));
19
+ writeJson(sessionsPath, data);
20
+ }
21
+ track(featureName, sessionId, taskFolder) {
22
+ const data = this.getSessions(featureName);
23
+ const now = new Date().toISOString();
24
+ let session = data.sessions.find(s => s.sessionId === sessionId);
25
+ if (session) {
26
+ session.lastActiveAt = now;
27
+ if (taskFolder)
28
+ session.taskFolder = taskFolder;
29
+ }
30
+ else {
31
+ session = {
32
+ sessionId,
33
+ taskFolder,
34
+ startedAt: now,
35
+ lastActiveAt: now,
36
+ };
37
+ data.sessions.push(session);
38
+ }
39
+ if (!data.master) {
40
+ data.master = sessionId;
41
+ }
42
+ this.saveSessions(featureName, data);
43
+ return session;
44
+ }
45
+ setMaster(featureName, sessionId) {
46
+ const data = this.getSessions(featureName);
47
+ data.master = sessionId;
48
+ this.saveSessions(featureName, data);
49
+ }
50
+ getMaster(featureName) {
51
+ return this.getSessions(featureName).master;
52
+ }
53
+ list(featureName) {
54
+ return this.getSessions(featureName).sessions;
55
+ }
56
+ get(featureName, sessionId) {
57
+ return this.getSessions(featureName).sessions.find(s => s.sessionId === sessionId);
58
+ }
59
+ getByTask(featureName, taskFolder) {
60
+ return this.getSessions(featureName).sessions.find(s => s.taskFolder === taskFolder);
61
+ }
62
+ remove(featureName, sessionId) {
63
+ const data = this.getSessions(featureName);
64
+ const index = data.sessions.findIndex(s => s.sessionId === sessionId);
65
+ if (index === -1)
66
+ return false;
67
+ data.sessions.splice(index, 1);
68
+ if (data.master === sessionId) {
69
+ data.master = data.sessions[0]?.sessionId;
70
+ }
71
+ this.saveSessions(featureName, data);
72
+ return true;
73
+ }
74
+ /**
75
+ * Find which feature a session belongs to by searching all features
76
+ */
77
+ findFeatureBySession(sessionId) {
78
+ const featuresPath = path.join(this.projectRoot, '.hive', 'features');
79
+ if (!fs.existsSync(featuresPath))
80
+ return null;
81
+ const features = fs.readdirSync(featuresPath, { withFileTypes: true })
82
+ .filter(d => d.isDirectory())
83
+ .map(d => d.name);
84
+ for (const feature of features) {
85
+ const sessions = this.getSessions(feature);
86
+ if (sessions.sessions.some(s => s.sessionId === sessionId)) {
87
+ return feature;
88
+ }
89
+ if (sessions.master === sessionId) {
90
+ return feature;
91
+ }
92
+ }
93
+ return null;
94
+ }
95
+ fork(featureName, fromSessionId) {
96
+ const data = this.getSessions(featureName);
97
+ const now = new Date().toISOString();
98
+ const sourceSession = fromSessionId
99
+ ? data.sessions.find(s => s.sessionId === fromSessionId)
100
+ : data.sessions.find(s => s.sessionId === data.master);
101
+ const newSessionId = `ses_fork_${Date.now()}`;
102
+ const newSession = {
103
+ sessionId: newSessionId,
104
+ taskFolder: sourceSession?.taskFolder,
105
+ startedAt: now,
106
+ lastActiveAt: now,
107
+ };
108
+ data.sessions.push(newSession);
109
+ this.saveSessions(featureName, data);
110
+ return newSession;
111
+ }
112
+ fresh(featureName, title) {
113
+ const data = this.getSessions(featureName);
114
+ const now = new Date().toISOString();
115
+ const newSessionId = `ses_${title ? title.replace(/\s+/g, '_').toLowerCase() : Date.now()}`;
116
+ const newSession = {
117
+ sessionId: newSessionId,
118
+ startedAt: now,
119
+ lastActiveAt: now,
120
+ };
121
+ data.sessions.push(newSession);
122
+ this.saveSessions(featureName, data);
123
+ return newSession;
124
+ }
125
+ }
@@ -5,7 +5,8 @@ export declare class TaskService {
5
5
  sync(featureName: string): TasksSyncResult;
6
6
  create(featureName: string, name: string, order?: number): string;
7
7
  private createFromPlan;
8
- update(featureName: string, taskFolder: string, updates: Partial<Pick<TaskStatus, 'status' | 'summary'>>): TaskStatus;
8
+ writeSpec(featureName: string, taskFolder: string, content: string): string;
9
+ update(featureName: string, taskFolder: string, updates: Partial<Pick<TaskStatus, 'status' | 'summary' | 'baseCommit'>>): TaskStatus;
9
10
  get(featureName: string, taskFolder: string): TaskInfo | null;
10
11
  list(featureName: string): TaskInfo[];
11
12
  writeReport(featureName: string, taskFolder: string, report: string): string;
@@ -1,5 +1,5 @@
1
1
  import * as fs from 'fs';
2
- import { getTasksPath, getTaskPath, getTaskStatusPath, getTaskReportPath, getPlanPath, ensureDir, readJson, writeJson, readText, writeText, fileExists, } from '../utils/paths.js';
2
+ import { getTasksPath, getTaskPath, getTaskStatusPath, getTaskReportPath, getTaskSpecPath, getPlanPath, ensureDir, readJson, writeJson, readText, writeText, fileExists, } from '../utils/paths.js';
3
3
  export class TaskService {
4
4
  projectRoot;
5
5
  constructor(projectRoot) {
@@ -45,7 +45,7 @@ export class TaskService {
45
45
  }
46
46
  for (const planTask of planTasks) {
47
47
  if (!existingByName.has(planTask.folder)) {
48
- this.createFromPlan(featureName, planTask.folder, planTask.order);
48
+ this.createFromPlan(featureName, planTask, planTasks);
49
49
  result.created.push(planTask.folder);
50
50
  }
51
51
  }
@@ -65,14 +65,55 @@ export class TaskService {
65
65
  writeJson(getTaskStatusPath(this.projectRoot, featureName, folder), status);
66
66
  return folder;
67
67
  }
68
- createFromPlan(featureName, folder, order) {
69
- const taskPath = getTaskPath(this.projectRoot, featureName, folder);
68
+ createFromPlan(featureName, task, allTasks) {
69
+ const taskPath = getTaskPath(this.projectRoot, featureName, task.folder);
70
70
  ensureDir(taskPath);
71
71
  const status = {
72
72
  status: 'pending',
73
73
  origin: 'plan',
74
74
  };
75
- writeJson(getTaskStatusPath(this.projectRoot, featureName, folder), status);
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;
76
117
  }
77
118
  update(featureName, taskFolder, updates) {
78
119
  const statusPath = getTaskStatusPath(this.projectRoot, featureName, taskFolder);
@@ -142,13 +183,47 @@ export class TaskService {
142
183
  }
143
184
  parseTasksFromPlan(content) {
144
185
  const tasks = [];
145
- const taskPattern = /^###\s+(\d+)\.\s+(.+)$/gm;
146
- let match;
147
- while ((match = taskPattern.exec(content)) !== null) {
148
- const order = parseInt(match[1], 10);
149
- const name = match[2].trim().toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
150
- const folder = `${String(order).padStart(2, '0')}-${name}`;
151
- tasks.push({ folder, order });
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);
152
227
  }
153
228
  return tasks;
154
229
  }
@@ -0,0 +1 @@
1
+ export {};