opencode-hive 0.2.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 ADDED
@@ -0,0 +1,91 @@
1
+ # opencode-hive
2
+
3
+ [![npm version](https://img.shields.io/npm/v/opencode-hive)](https://www.npmjs.com/package/opencode-hive)
4
+ [![License: MIT with Commons Clause](https://img.shields.io/badge/License-MIT%20with%20Commons%20Clause-blue.svg)](../../LICENSE)
5
+
6
+ **From Vibe Coding to Hive Coding** — The OpenCode plugin that brings structure to AI-assisted development.
7
+
8
+ ## Why Hive?
9
+
10
+ Stop losing context. Stop repeating decisions. Start shipping with confidence.
11
+
12
+ ```
13
+ Vibe: "Just make it work"
14
+ Hive: Plan → Review → Approve → Execute → Ship
15
+ ```
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install opencode-hive
21
+ ```
22
+
23
+ ## The Workflow
24
+
25
+ 1. **Create Feature** — `hive_feature_create("dark-mode")`
26
+ 2. **Write Plan** — AI generates structured plan
27
+ 3. **Review** — You review in VS Code, add comments
28
+ 4. **Approve** — `hive_plan_approve()`
29
+ 5. **Execute** — Tasks run in isolated git worktrees
30
+ 6. **Ship** — Clean commits, full audit trail
31
+
32
+ ## Tools
33
+
34
+ ### Feature Management
35
+ | Tool | Description |
36
+ |------|-------------|
37
+ | `hive_feature_create` | Create a new feature |
38
+ | `hive_feature_list` | List all features |
39
+ | `hive_feature_switch` | Switch to a feature |
40
+ | `hive_feature_complete` | Mark feature as complete |
41
+ | `hive_status` | Get feature overview |
42
+
43
+ ### Planning
44
+ | Tool | Description |
45
+ |------|-------------|
46
+ | `hive_plan_write` | Write plan.md |
47
+ | `hive_plan_read` | Read plan and comments |
48
+ | `hive_plan_approve` | Approve plan for execution |
49
+
50
+ ### Tasks
51
+ | Tool | Description |
52
+ |------|-------------|
53
+ | `hive_tasks_sync` | Generate tasks from plan |
54
+ | `hive_task_create` | Create manual task |
55
+ | `hive_task_update` | Update task status/summary |
56
+
57
+ ### Execution
58
+ | Tool | Description |
59
+ |------|-------------|
60
+ | `hive_exec_start` | Start work on task (creates worktree) |
61
+ | `hive_exec_complete` | Complete task (applies changes) |
62
+ | `hive_exec_abort` | Abort task (discard changes) |
63
+
64
+ ## Plan Format
65
+
66
+ ```markdown
67
+ # Feature Name
68
+
69
+ ## Overview
70
+ What we're building and why.
71
+
72
+ ## Tasks
73
+
74
+ ### 1. Task Name
75
+ Description of what to do.
76
+
77
+ ### 2. Another Task
78
+ Description.
79
+ ```
80
+
81
+ ## Pair with VS Code
82
+
83
+ For the full experience, install [vscode-hive](https://marketplace.visualstudio.com/items?itemName=tctinh.vscode-hive) to review plans inline with comments.
84
+
85
+ ## License
86
+
87
+ MIT with Commons Clause — Free for personal and non-commercial use. See [LICENSE](../../LICENSE) for details.
88
+
89
+ ---
90
+
91
+ **Stop vibing. Start hiving.** 🐝
@@ -0,0 +1,3 @@
1
+ import { type Plugin } from "@opencode-ai/plugin";
2
+ declare const plugin: Plugin;
3
+ export default plugin;
package/dist/index.js ADDED
@@ -0,0 +1,307 @@
1
+ import * as path from 'path';
2
+ import { tool } from "@opencode-ai/plugin";
3
+ import { WorktreeService } from "./services/worktreeService.js";
4
+ import { FeatureService } from "./services/featureService.js";
5
+ import { PlanService } from "./services/planService.js";
6
+ import { TaskService } from "./services/taskService.js";
7
+ const HIVE_SYSTEM_PROMPT = `
8
+ ## Hive - Feature Development System
9
+
10
+ Plan-first development: Write plan → User reviews → Approve → Execute tasks
11
+
12
+ ### Tools (13 total)
13
+
14
+ | Domain | Tools |
15
+ |--------|-------|
16
+ | Feature | hive_feature_create, hive_feature_list, hive_feature_switch, hive_feature_complete, hive_status |
17
+ | Plan | hive_plan_write, hive_plan_read, hive_plan_approve |
18
+ | Task | hive_tasks_sync, hive_task_create, hive_task_update |
19
+ | Exec | hive_exec_start, hive_exec_complete, hive_exec_abort |
20
+
21
+ ### Workflow
22
+
23
+ 1. \`hive_feature_create(name)\` - Create feature
24
+ 2. \`hive_plan_write(content)\` - Write plan.md
25
+ 3. User adds comments in VSCode → \`hive_plan_read\` to see them
26
+ 4. Revise plan → User approves
27
+ 5. \`hive_tasks_sync()\` - Generate tasks from plan
28
+ 6. \`hive_exec_start(task)\` → work → \`hive_exec_complete(task, summary)\`
29
+
30
+ ### Plan Format
31
+
32
+ \`\`\`markdown
33
+ # Feature Name
34
+
35
+ ## Overview
36
+ What we're building and why.
37
+
38
+ ## Tasks
39
+
40
+ ### 1. Task Name
41
+ Description of what to do.
42
+
43
+ ### 2. Another Task
44
+ Description.
45
+ \`\`\`
46
+
47
+ \`hive_tasks_sync\` parses \`### N. Task Name\` headers.
48
+ `;
49
+ const plugin = async (ctx) => {
50
+ const { directory } = ctx;
51
+ const featureService = new FeatureService(directory);
52
+ const planService = new PlanService(directory);
53
+ const taskService = new TaskService(directory);
54
+ const worktreeService = new WorktreeService({
55
+ baseDir: directory,
56
+ hiveDir: path.join(directory, '.hive'),
57
+ });
58
+ const captureSession = (toolContext) => {
59
+ const activeFeature = featureService.getActive();
60
+ if (!activeFeature)
61
+ return;
62
+ const ctx = toolContext;
63
+ if (ctx?.sessionID) {
64
+ const currentSession = featureService.getSession(activeFeature);
65
+ if (currentSession !== ctx.sessionID) {
66
+ featureService.setSession(activeFeature, ctx.sessionID);
67
+ }
68
+ }
69
+ };
70
+ return {
71
+ "experimental.chat.system.transform": async (_input, output) => {
72
+ output.system.push(HIVE_SYSTEM_PROMPT);
73
+ const activeFeature = featureService.getActive();
74
+ if (activeFeature) {
75
+ const info = featureService.getInfo(activeFeature);
76
+ if (info) {
77
+ let statusHint = `\n### Current Hive Status\n`;
78
+ statusHint += `**Active Feature**: ${info.name} (${info.status})\n`;
79
+ statusHint += `**Progress**: ${info.tasks.filter(t => t.status === 'done').length}/${info.tasks.length} tasks\n`;
80
+ if (info.commentCount > 0) {
81
+ statusHint += `**Comments**: ${info.commentCount} unresolved - address with hive_plan_read\n`;
82
+ }
83
+ output.system.push(statusHint);
84
+ }
85
+ }
86
+ },
87
+ tool: {
88
+ hive_feature_create: tool({
89
+ description: 'Create a new feature and set it as active',
90
+ args: {
91
+ name: tool.schema.string().describe('Feature name'),
92
+ ticket: tool.schema.string().optional().describe('Ticket reference'),
93
+ },
94
+ async execute({ name, ticket }) {
95
+ const feature = featureService.create(name, ticket);
96
+ return `Feature "${name}" created. Status: ${feature.status}. Write a plan with hive_plan_write.`;
97
+ },
98
+ }),
99
+ hive_feature_list: tool({
100
+ description: 'List all features',
101
+ args: {},
102
+ async execute() {
103
+ const features = featureService.list();
104
+ const active = featureService.getActive();
105
+ if (features.length === 0)
106
+ return "No features found.";
107
+ const list = features.map(f => {
108
+ const info = featureService.getInfo(f);
109
+ return `${f === active ? '* ' : ' '}${f} (${info?.status || 'unknown'})`;
110
+ });
111
+ return list.join('\n');
112
+ },
113
+ }),
114
+ hive_feature_switch: tool({
115
+ description: 'Switch to a different feature',
116
+ args: { name: tool.schema.string().describe('Feature name') },
117
+ async execute({ name }) {
118
+ featureService.setActive(name);
119
+ return `Switched to feature "${name}"`;
120
+ },
121
+ }),
122
+ hive_feature_complete: tool({
123
+ description: 'Mark feature as completed (irreversible)',
124
+ args: { name: tool.schema.string().optional().describe('Feature name (defaults to active)') },
125
+ async execute({ name }) {
126
+ const feature = name || featureService.getActive();
127
+ if (!feature)
128
+ return "Error: No active feature";
129
+ featureService.complete(feature);
130
+ return `Feature "${feature}" marked as completed`;
131
+ },
132
+ }),
133
+ hive_status: tool({
134
+ description: 'Get overview of active feature',
135
+ args: { name: tool.schema.string().optional().describe('Feature name (defaults to active)') },
136
+ async execute({ name }) {
137
+ const feature = name || featureService.getActive();
138
+ if (!feature)
139
+ return "Error: No active feature";
140
+ const info = featureService.getInfo(feature);
141
+ if (!info)
142
+ return `Error: Feature "${feature}" not found`;
143
+ return JSON.stringify(info, null, 2);
144
+ },
145
+ }),
146
+ hive_plan_write: tool({
147
+ description: 'Write plan.md (clears existing comments)',
148
+ args: { content: tool.schema.string().describe('Plan markdown content') },
149
+ async execute({ content }, toolContext) {
150
+ captureSession(toolContext);
151
+ const feature = featureService.getActive();
152
+ if (!feature)
153
+ return "Error: No active feature";
154
+ const planPath = planService.write(feature, content);
155
+ return `Plan written to ${planPath}. Comments cleared for fresh review.`;
156
+ },
157
+ }),
158
+ hive_plan_read: tool({
159
+ description: 'Read plan.md and user comments',
160
+ args: {},
161
+ async execute(_args, toolContext) {
162
+ captureSession(toolContext);
163
+ const feature = featureService.getActive();
164
+ if (!feature)
165
+ return "Error: No active feature";
166
+ const result = planService.read(feature);
167
+ if (!result)
168
+ return "Error: No plan.md found";
169
+ return JSON.stringify(result, null, 2);
170
+ },
171
+ }),
172
+ hive_plan_approve: tool({
173
+ description: 'Approve plan for execution',
174
+ args: {},
175
+ async execute(_args, toolContext) {
176
+ captureSession(toolContext);
177
+ const feature = featureService.getActive();
178
+ if (!feature)
179
+ return "Error: No active feature";
180
+ const comments = planService.getComments(feature);
181
+ if (comments.length > 0) {
182
+ return `Error: Cannot approve - ${comments.length} unresolved comment(s). Address them first.`;
183
+ }
184
+ planService.approve(feature);
185
+ return "Plan approved. Run hive_tasks_sync to generate tasks.";
186
+ },
187
+ }),
188
+ hive_tasks_sync: tool({
189
+ description: 'Generate tasks from approved plan',
190
+ args: {},
191
+ async execute() {
192
+ const feature = featureService.getActive();
193
+ if (!feature)
194
+ return "Error: No active feature";
195
+ const featureData = featureService.get(feature);
196
+ if (!featureData || featureData.status === 'planning') {
197
+ return "Error: Plan must be approved first";
198
+ }
199
+ const result = taskService.sync(feature);
200
+ if (featureData.status === 'approved') {
201
+ featureService.updateStatus(feature, 'executing');
202
+ }
203
+ return `Tasks synced: ${result.created.length} created, ${result.removed.length} removed, ${result.kept.length} kept`;
204
+ },
205
+ }),
206
+ hive_task_create: tool({
207
+ description: 'Create manual task (not from plan)',
208
+ args: {
209
+ name: tool.schema.string().describe('Task name'),
210
+ order: tool.schema.number().optional().describe('Task order'),
211
+ },
212
+ async execute({ name, order }) {
213
+ const feature = featureService.getActive();
214
+ if (!feature)
215
+ return "Error: No active feature";
216
+ const folder = taskService.create(feature, name, order);
217
+ return `Manual task created: ${folder}`;
218
+ },
219
+ }),
220
+ hive_task_update: tool({
221
+ description: 'Update task status or summary',
222
+ args: {
223
+ task: tool.schema.string().describe('Task folder name'),
224
+ status: tool.schema.string().optional().describe('New status: pending, in_progress, done, cancelled'),
225
+ summary: tool.schema.string().optional().describe('Summary of work'),
226
+ },
227
+ async execute({ task, status, summary }) {
228
+ const feature = featureService.getActive();
229
+ if (!feature)
230
+ return "Error: No active feature";
231
+ const updated = taskService.update(feature, task, {
232
+ status: status,
233
+ summary,
234
+ });
235
+ return `Task "${task}" updated: status=${updated.status}`;
236
+ },
237
+ }),
238
+ hive_exec_start: tool({
239
+ description: 'Create worktree and begin work on task',
240
+ args: { task: tool.schema.string().describe('Task folder name') },
241
+ async execute({ task }) {
242
+ const feature = featureService.getActive();
243
+ if (!feature)
244
+ return "Error: No active feature";
245
+ const taskInfo = taskService.get(feature, task);
246
+ if (!taskInfo)
247
+ return `Error: Task "${task}" not found`;
248
+ if (taskInfo.status === 'done')
249
+ return "Error: Task already completed";
250
+ const worktree = await worktreeService.create(feature, task);
251
+ taskService.update(feature, task, { status: 'in_progress' });
252
+ return `Worktree created at ${worktree.path}\nBranch: ${worktree.branch}`;
253
+ },
254
+ }),
255
+ hive_exec_complete: tool({
256
+ description: 'Complete task: apply changes, write report',
257
+ args: {
258
+ task: tool.schema.string().describe('Task folder name'),
259
+ summary: tool.schema.string().describe('Summary of what was done'),
260
+ },
261
+ async execute({ task, summary }) {
262
+ const feature = featureService.getActive();
263
+ if (!feature)
264
+ return "Error: No active feature";
265
+ const taskInfo = taskService.get(feature, task);
266
+ if (!taskInfo)
267
+ return `Error: Task "${task}" not found`;
268
+ if (taskInfo.status !== 'in_progress')
269
+ return "Error: Task not in progress";
270
+ const diff = await worktreeService.getDiff(feature, task);
271
+ if (diff?.hasDiff) {
272
+ await worktreeService.applyDiff(feature, task);
273
+ }
274
+ const report = `# ${task}\n\n## Summary\n\n${summary}\n`;
275
+ taskService.writeReport(feature, task, report);
276
+ taskService.update(feature, task, { status: 'done', summary });
277
+ await worktreeService.remove(feature, task);
278
+ return `Task "${task}" completed. Changes applied.`;
279
+ },
280
+ }),
281
+ hive_exec_abort: tool({
282
+ description: 'Abort task: discard changes, reset status',
283
+ args: { task: tool.schema.string().describe('Task folder name') },
284
+ async execute({ task }) {
285
+ const feature = featureService.getActive();
286
+ if (!feature)
287
+ return "Error: No active feature";
288
+ await worktreeService.remove(feature, task);
289
+ taskService.update(feature, task, { status: 'pending' });
290
+ return `Task "${task}" aborted. Status reset to pending.`;
291
+ },
292
+ }),
293
+ },
294
+ command: {
295
+ hive: {
296
+ description: "Create a new feature: /hive <feature-name>",
297
+ async run(args) {
298
+ const name = args.trim();
299
+ if (!name)
300
+ return "Usage: /hive <feature-name>";
301
+ return `Create feature "${name}" using hive_feature_create tool.`;
302
+ },
303
+ },
304
+ },
305
+ };
306
+ };
307
+ export default plugin;
@@ -0,0 +1,16 @@
1
+ import { FeatureJson, FeatureStatusType, FeatureInfo } from '../types.js';
2
+ export declare class FeatureService {
3
+ private projectRoot;
4
+ constructor(projectRoot: string);
5
+ create(name: string, ticket?: string): FeatureJson;
6
+ get(name: string): FeatureJson | null;
7
+ list(): string[];
8
+ getActive(): string | null;
9
+ setActive(name: string): void;
10
+ updateStatus(name: string, status: FeatureStatusType): FeatureJson;
11
+ getInfo(name: string): FeatureInfo | null;
12
+ private getTasks;
13
+ complete(name: string): FeatureJson;
14
+ setSession(name: string, sessionId: string): void;
15
+ getSession(name: string): string | undefined;
16
+ }
@@ -0,0 +1,117 @@
1
+ import * as fs from 'fs';
2
+ import { getFeaturePath, getFeaturesPath, getFeatureJsonPath, getContextPath, getTasksPath, getActiveFeaturePath, getPlanPath, getCommentsPath, ensureDir, readJson, writeJson, readText, writeText, fileExists, } from '../utils/paths.js';
3
+ export class FeatureService {
4
+ projectRoot;
5
+ constructor(projectRoot) {
6
+ this.projectRoot = projectRoot;
7
+ }
8
+ create(name, ticket) {
9
+ const featurePath = getFeaturePath(this.projectRoot, name);
10
+ if (fileExists(featurePath)) {
11
+ throw new Error(`Feature '${name}' already exists`);
12
+ }
13
+ ensureDir(featurePath);
14
+ ensureDir(getContextPath(this.projectRoot, name));
15
+ ensureDir(getTasksPath(this.projectRoot, name));
16
+ const feature = {
17
+ name,
18
+ status: 'planning',
19
+ ticket,
20
+ createdAt: new Date().toISOString(),
21
+ };
22
+ writeJson(getFeatureJsonPath(this.projectRoot, name), feature);
23
+ this.setActive(name);
24
+ return feature;
25
+ }
26
+ get(name) {
27
+ return readJson(getFeatureJsonPath(this.projectRoot, name));
28
+ }
29
+ list() {
30
+ const featuresPath = getFeaturesPath(this.projectRoot);
31
+ if (!fileExists(featuresPath))
32
+ return [];
33
+ return fs.readdirSync(featuresPath, { withFileTypes: true })
34
+ .filter(d => d.isDirectory())
35
+ .map(d => d.name);
36
+ }
37
+ getActive() {
38
+ return readText(getActiveFeaturePath(this.projectRoot))?.trim() || null;
39
+ }
40
+ setActive(name) {
41
+ const feature = this.get(name);
42
+ if (!feature)
43
+ throw new Error(`Feature '${name}' not found`);
44
+ writeText(getActiveFeaturePath(this.projectRoot), name);
45
+ }
46
+ updateStatus(name, status) {
47
+ const feature = this.get(name);
48
+ if (!feature)
49
+ throw new Error(`Feature '${name}' not found`);
50
+ feature.status = status;
51
+ if (status === 'approved' && !feature.approvedAt) {
52
+ feature.approvedAt = new Date().toISOString();
53
+ }
54
+ if (status === 'completed' && !feature.completedAt) {
55
+ feature.completedAt = new Date().toISOString();
56
+ }
57
+ writeJson(getFeatureJsonPath(this.projectRoot, name), feature);
58
+ return feature;
59
+ }
60
+ getInfo(name) {
61
+ const feature = this.get(name);
62
+ if (!feature)
63
+ return null;
64
+ const tasks = this.getTasks(name);
65
+ const hasPlan = fileExists(getPlanPath(this.projectRoot, name));
66
+ const comments = readJson(getCommentsPath(this.projectRoot, name));
67
+ const commentCount = comments?.threads?.length || 0;
68
+ return {
69
+ name: feature.name,
70
+ status: feature.status,
71
+ tasks,
72
+ hasPlan,
73
+ commentCount,
74
+ };
75
+ }
76
+ getTasks(featureName) {
77
+ const tasksPath = getTasksPath(this.projectRoot, featureName);
78
+ if (!fileExists(tasksPath))
79
+ return [];
80
+ const folders = fs.readdirSync(tasksPath, { withFileTypes: true })
81
+ .filter(d => d.isDirectory())
82
+ .map(d => d.name)
83
+ .sort();
84
+ return folders.map(folder => {
85
+ const statusPath = `${tasksPath}/${folder}/status.json`;
86
+ const status = readJson(statusPath);
87
+ const name = folder.replace(/^\d+-/, '');
88
+ return {
89
+ folder,
90
+ name,
91
+ status: status?.status || 'pending',
92
+ origin: status?.origin || 'plan',
93
+ summary: status?.summary,
94
+ };
95
+ });
96
+ }
97
+ complete(name) {
98
+ const feature = this.get(name);
99
+ if (!feature)
100
+ throw new Error(`Feature '${name}' not found`);
101
+ if (feature.status === 'completed') {
102
+ throw new Error(`Feature '${name}' is already completed`);
103
+ }
104
+ return this.updateStatus(name, 'completed');
105
+ }
106
+ setSession(name, sessionId) {
107
+ const feature = this.get(name);
108
+ if (!feature)
109
+ throw new Error(`Feature '${name}' not found`);
110
+ feature.sessionId = sessionId;
111
+ writeJson(getFeatureJsonPath(this.projectRoot, name), feature);
112
+ }
113
+ getSession(name) {
114
+ const feature = this.get(name);
115
+ return feature?.sessionId;
116
+ }
117
+ }
@@ -0,0 +1,5 @@
1
+ export { WorktreeService, createWorktreeService } from "./worktreeService.js";
2
+ export type { WorktreeInfo, DiffResult, ApplyResult, WorktreeConfig } from "./worktreeService.js";
3
+ export { FeatureService } from "./featureService.js";
4
+ export { PlanService } from "./planService.js";
5
+ export { TaskService } from "./taskService.js";
@@ -0,0 +1,4 @@
1
+ export { WorktreeService, createWorktreeService } from "./worktreeService.js";
2
+ export { FeatureService } from "./featureService.js";
3
+ export { PlanService } from "./planService.js";
4
+ export { TaskService } from "./taskService.js";
@@ -0,0 +1,11 @@
1
+ import { PlanComment, PlanReadResult } from '../types.js';
2
+ export declare class PlanService {
3
+ private projectRoot;
4
+ constructor(projectRoot: string);
5
+ write(featureName: string, content: string): string;
6
+ read(featureName: string): PlanReadResult | null;
7
+ approve(featureName: string): void;
8
+ getComments(featureName: string): PlanComment[];
9
+ addComment(featureName: string, comment: Omit<PlanComment, 'id' | 'timestamp'>): PlanComment;
10
+ clearComments(featureName: string): void;
11
+ }
@@ -0,0 +1,59 @@
1
+ import { getPlanPath, getCommentsPath, getFeatureJsonPath, readJson, writeJson, readText, writeText, fileExists, } from '../utils/paths.js';
2
+ export class PlanService {
3
+ projectRoot;
4
+ constructor(projectRoot) {
5
+ this.projectRoot = projectRoot;
6
+ }
7
+ write(featureName, content) {
8
+ const planPath = getPlanPath(this.projectRoot, featureName);
9
+ writeText(planPath, content);
10
+ this.clearComments(featureName);
11
+ return planPath;
12
+ }
13
+ read(featureName) {
14
+ const planPath = getPlanPath(this.projectRoot, featureName);
15
+ const content = readText(planPath);
16
+ if (content === null)
17
+ return null;
18
+ const feature = readJson(getFeatureJsonPath(this.projectRoot, featureName));
19
+ const comments = this.getComments(featureName);
20
+ return {
21
+ content,
22
+ status: feature?.status || 'planning',
23
+ comments,
24
+ };
25
+ }
26
+ approve(featureName) {
27
+ const featurePath = getFeatureJsonPath(this.projectRoot, featureName);
28
+ const feature = readJson(featurePath);
29
+ if (!feature)
30
+ throw new Error(`Feature '${featureName}' not found`);
31
+ if (!fileExists(getPlanPath(this.projectRoot, featureName))) {
32
+ throw new Error(`No plan.md found for feature '${featureName}'`);
33
+ }
34
+ feature.status = 'approved';
35
+ feature.approvedAt = new Date().toISOString();
36
+ writeJson(featurePath, feature);
37
+ }
38
+ getComments(featureName) {
39
+ const commentsPath = getCommentsPath(this.projectRoot, featureName);
40
+ const data = readJson(commentsPath);
41
+ return data?.threads || [];
42
+ }
43
+ addComment(featureName, comment) {
44
+ const commentsPath = getCommentsPath(this.projectRoot, featureName);
45
+ const data = readJson(commentsPath) || { threads: [] };
46
+ const newComment = {
47
+ ...comment,
48
+ id: `comment-${Date.now()}`,
49
+ timestamp: new Date().toISOString(),
50
+ };
51
+ data.threads.push(newComment);
52
+ writeJson(commentsPath, data);
53
+ return newComment;
54
+ }
55
+ clearComments(featureName) {
56
+ const commentsPath = getCommentsPath(this.projectRoot, featureName);
57
+ writeJson(commentsPath, { threads: [] });
58
+ }
59
+ }
@@ -0,0 +1,16 @@
1
+ import { TaskStatus, TasksSyncResult, TaskInfo } from '../types.js';
2
+ export declare class TaskService {
3
+ private projectRoot;
4
+ constructor(projectRoot: string);
5
+ sync(featureName: string): TasksSyncResult;
6
+ create(featureName: string, name: string, order?: number): string;
7
+ private createFromPlan;
8
+ update(featureName: string, taskFolder: string, updates: Partial<Pick<TaskStatus, 'status' | 'summary'>>): TaskStatus;
9
+ get(featureName: string, taskFolder: string): TaskInfo | null;
10
+ list(featureName: string): TaskInfo[];
11
+ writeReport(featureName: string, taskFolder: string, report: string): string;
12
+ private listFolders;
13
+ private deleteTask;
14
+ private getNextOrder;
15
+ private parseTasksFromPlan;
16
+ }