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 +91 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +307 -0
- package/dist/services/featureService.d.ts +16 -0
- package/dist/services/featureService.js +117 -0
- package/dist/services/index.d.ts +5 -0
- package/dist/services/index.js +4 -0
- package/dist/services/planService.d.ts +11 -0
- package/dist/services/planService.js +59 -0
- package/dist/services/taskService.d.ts +16 -0
- package/dist/services/taskService.js +155 -0
- package/dist/services/worktreeService.d.ts +49 -0
- package/dist/services/worktreeService.js +331 -0
- package/dist/tools/execTools.d.ts +72 -0
- package/dist/tools/execTools.js +123 -0
- package/dist/tools/featureTools.d.ts +104 -0
- package/dist/tools/featureTools.js +113 -0
- package/dist/tools/planTools.d.ts +57 -0
- package/dist/tools/planTools.js +65 -0
- package/dist/tools/taskTools.d.ts +91 -0
- package/dist/tools/taskTools.js +87 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.js +1 -0
- package/dist/utils/paths.d.ts +18 -0
- package/dist/utils/paths.js +75 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# opencode-hive
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/opencode-hive)
|
|
4
|
+
[](../../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.** 🐝
|
package/dist/index.d.ts
ADDED
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,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
|
+
}
|