opencode-hive 0.4.4 → 0.5.1
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 +0 -2
- package/dist/e2e/opencode-runtime-smoke.test.d.ts +1 -0
- package/dist/e2e/opencode-runtime-smoke.test.js +243 -0
- package/dist/e2e/plugin-smoke.test.d.ts +1 -0
- package/dist/e2e/plugin-smoke.test.js +127 -0
- package/dist/index.js +273 -62
- package/dist/services/contextService.d.ts +15 -0
- package/dist/services/contextService.js +59 -0
- package/dist/services/featureService.d.ts +0 -2
- package/dist/services/featureService.js +1 -11
- package/dist/services/featureService.test.d.ts +1 -0
- package/dist/services/featureService.test.js +127 -0
- package/dist/services/planService.test.d.ts +1 -0
- package/dist/services/planService.test.js +115 -0
- package/dist/services/sessionService.d.ts +31 -0
- package/dist/services/sessionService.js +125 -0
- package/dist/services/taskService.d.ts +2 -1
- package/dist/services/taskService.js +87 -12
- package/dist/services/taskService.test.d.ts +1 -0
- package/dist/services/taskService.test.js +159 -0
- package/dist/services/worktreeService.js +42 -17
- package/dist/services/worktreeService.test.d.ts +1 -0
- package/dist/services/worktreeService.test.js +117 -0
- package/dist/tools/contextTools.d.ts +93 -0
- package/dist/tools/contextTools.js +83 -0
- package/dist/tools/execTools.js +8 -3
- package/dist/tools/featureTools.d.ts +0 -14
- package/dist/tools/featureTools.js +7 -18
- package/dist/tools/planTools.js +8 -3
- package/dist/tools/sessionTools.d.ts +35 -0
- package/dist/tools/sessionTools.js +95 -0
- package/dist/tools/taskTools.js +9 -4
- package/dist/types.d.ts +35 -0
- package/dist/utils/detection.d.ts +12 -0
- package/dist/utils/detection.js +73 -0
- package/dist/utils/paths.d.ts +1 -1
- package/dist/utils/paths.js +2 -3
- package/dist/utils/paths.test.d.ts +1 -0
- package/dist/utils/paths.test.js +100 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,19 +4,24 @@ import { WorktreeService } from "./services/worktreeService.js";
|
|
|
4
4
|
import { FeatureService } from "./services/featureService.js";
|
|
5
5
|
import { PlanService } from "./services/planService.js";
|
|
6
6
|
import { TaskService } from "./services/taskService.js";
|
|
7
|
+
import { ContextService } from "./services/contextService.js";
|
|
8
|
+
import { SessionService } from "./services/sessionService.js";
|
|
9
|
+
import { detectContext, listFeatures } from "./utils/detection.js";
|
|
7
10
|
const HIVE_SYSTEM_PROMPT = `
|
|
8
11
|
## Hive - Feature Development System
|
|
9
12
|
|
|
10
13
|
Plan-first development: Write plan → User reviews → Approve → Execute tasks
|
|
11
14
|
|
|
12
|
-
### Tools (
|
|
15
|
+
### Tools (16 total)
|
|
13
16
|
|
|
14
17
|
| Domain | Tools |
|
|
15
18
|
|--------|-------|
|
|
16
|
-
| Feature | hive_feature_create, hive_feature_list,
|
|
19
|
+
| Feature | hive_feature_create, hive_feature_list, hive_feature_complete |
|
|
17
20
|
| Plan | hive_plan_write, hive_plan_read, hive_plan_approve |
|
|
18
21
|
| Task | hive_tasks_sync, hive_task_create, hive_task_update |
|
|
19
22
|
| Exec | hive_exec_start, hive_exec_complete, hive_exec_abort |
|
|
23
|
+
| Context | hive_context_write, hive_context_read, hive_context_list |
|
|
24
|
+
| Session | hive_session_open, hive_session_list |
|
|
20
25
|
|
|
21
26
|
### Workflow
|
|
22
27
|
|
|
@@ -44,6 +49,19 @@ Description of what to do.
|
|
|
44
49
|
Description.
|
|
45
50
|
\`\`\`
|
|
46
51
|
|
|
52
|
+
### Planning Phase - Context Management REQUIRED
|
|
53
|
+
|
|
54
|
+
As you research and plan, CONTINUOUSLY save findings using \`hive_context_write\`:
|
|
55
|
+
- Research findings (API patterns, library docs, codebase structure)
|
|
56
|
+
- User preferences ("we use Zustand, not Redux")
|
|
57
|
+
- Rejected alternatives ("tried X, too complex")
|
|
58
|
+
- Architecture decisions ("auth lives in /lib/auth")
|
|
59
|
+
|
|
60
|
+
**Update existing context files** when new info emerges - dont create duplicates.
|
|
61
|
+
Workers depend on context for background. Without it, they work blind.
|
|
62
|
+
|
|
63
|
+
Save context BEFORE writing the plan, and UPDATE it as planning iterates.
|
|
64
|
+
|
|
47
65
|
\`hive_tasks_sync\` parses \`### N. Task Name\` headers.
|
|
48
66
|
`;
|
|
49
67
|
const plugin = async (ctx) => {
|
|
@@ -51,26 +69,36 @@ const plugin = async (ctx) => {
|
|
|
51
69
|
const featureService = new FeatureService(directory);
|
|
52
70
|
const planService = new PlanService(directory);
|
|
53
71
|
const taskService = new TaskService(directory);
|
|
72
|
+
const contextService = new ContextService(directory);
|
|
73
|
+
const sessionService = new SessionService(directory);
|
|
54
74
|
const worktreeService = new WorktreeService({
|
|
55
75
|
baseDir: directory,
|
|
56
76
|
hiveDir: path.join(directory, '.hive'),
|
|
57
77
|
});
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
78
|
+
const resolveFeature = (explicit) => {
|
|
79
|
+
if (explicit)
|
|
80
|
+
return explicit;
|
|
81
|
+
const context = detectContext(directory);
|
|
82
|
+
if (context.feature)
|
|
83
|
+
return context.feature;
|
|
84
|
+
const features = listFeatures(directory);
|
|
85
|
+
if (features.length === 1)
|
|
86
|
+
return features[0];
|
|
87
|
+
return null;
|
|
88
|
+
};
|
|
89
|
+
const captureSession = (feature, toolContext) => {
|
|
62
90
|
const ctx = toolContext;
|
|
63
91
|
if (ctx?.sessionID) {
|
|
64
|
-
const currentSession = featureService.getSession(
|
|
92
|
+
const currentSession = featureService.getSession(feature);
|
|
65
93
|
if (currentSession !== ctx.sessionID) {
|
|
66
|
-
featureService.setSession(
|
|
94
|
+
featureService.setSession(feature, ctx.sessionID);
|
|
67
95
|
}
|
|
68
96
|
}
|
|
69
97
|
};
|
|
70
98
|
return {
|
|
71
99
|
"experimental.chat.system.transform": async (_input, output) => {
|
|
72
100
|
output.system.push(HIVE_SYSTEM_PROMPT);
|
|
73
|
-
const activeFeature =
|
|
101
|
+
const activeFeature = resolveFeature();
|
|
74
102
|
if (activeFeature) {
|
|
75
103
|
const info = featureService.getInfo(activeFeature);
|
|
76
104
|
if (info) {
|
|
@@ -101,7 +129,7 @@ const plugin = async (ctx) => {
|
|
|
101
129
|
args: {},
|
|
102
130
|
async execute() {
|
|
103
131
|
const features = featureService.list();
|
|
104
|
-
const active =
|
|
132
|
+
const active = resolveFeature();
|
|
105
133
|
if (features.length === 0)
|
|
106
134
|
return "No features found.";
|
|
107
135
|
const list = features.map(f => {
|
|
@@ -111,45 +139,42 @@ const plugin = async (ctx) => {
|
|
|
111
139
|
return list.join('\n');
|
|
112
140
|
},
|
|
113
141
|
}),
|
|
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
142
|
hive_feature_complete: tool({
|
|
123
143
|
description: 'Mark feature as completed (irreversible)',
|
|
124
144
|
args: { name: tool.schema.string().optional().describe('Feature name (defaults to active)') },
|
|
125
145
|
async execute({ name }) {
|
|
126
|
-
const feature = name
|
|
146
|
+
const feature = resolveFeature(name);
|
|
127
147
|
if (!feature)
|
|
128
|
-
return "Error: No
|
|
148
|
+
return "Error: No feature specified. Create a feature or provide name.";
|
|
129
149
|
featureService.complete(feature);
|
|
130
150
|
return `Feature "${feature}" marked as completed`;
|
|
131
151
|
},
|
|
132
152
|
}),
|
|
133
153
|
hive_plan_write: tool({
|
|
134
154
|
description: 'Write plan.md (clears existing comments)',
|
|
135
|
-
args: {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
155
|
+
args: {
|
|
156
|
+
content: tool.schema.string().describe('Plan markdown content'),
|
|
157
|
+
feature: tool.schema.string().optional().describe('Feature name (defaults to detection or single feature)'),
|
|
158
|
+
},
|
|
159
|
+
async execute({ content, feature: explicitFeature }, toolContext) {
|
|
160
|
+
const feature = resolveFeature(explicitFeature);
|
|
139
161
|
if (!feature)
|
|
140
|
-
return "Error: No
|
|
162
|
+
return "Error: No feature specified. Create a feature or provide feature param.";
|
|
163
|
+
captureSession(feature, toolContext);
|
|
141
164
|
const planPath = planService.write(feature, content);
|
|
142
165
|
return `Plan written to ${planPath}. Comments cleared for fresh review.`;
|
|
143
166
|
},
|
|
144
167
|
}),
|
|
145
168
|
hive_plan_read: tool({
|
|
146
169
|
description: 'Read plan.md and user comments',
|
|
147
|
-
args: {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
170
|
+
args: {
|
|
171
|
+
feature: tool.schema.string().optional().describe('Feature name (defaults to detection or single feature)'),
|
|
172
|
+
},
|
|
173
|
+
async execute({ feature: explicitFeature }, toolContext) {
|
|
174
|
+
const feature = resolveFeature(explicitFeature);
|
|
151
175
|
if (!feature)
|
|
152
|
-
return "Error: No
|
|
176
|
+
return "Error: No feature specified. Create a feature or provide feature param.";
|
|
177
|
+
captureSession(feature, toolContext);
|
|
153
178
|
const result = planService.read(feature);
|
|
154
179
|
if (!result)
|
|
155
180
|
return "Error: No plan.md found";
|
|
@@ -158,12 +183,14 @@ const plugin = async (ctx) => {
|
|
|
158
183
|
}),
|
|
159
184
|
hive_plan_approve: tool({
|
|
160
185
|
description: 'Approve plan for execution',
|
|
161
|
-
args: {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
186
|
+
args: {
|
|
187
|
+
feature: tool.schema.string().optional().describe('Feature name (defaults to detection or single feature)'),
|
|
188
|
+
},
|
|
189
|
+
async execute({ feature: explicitFeature }, toolContext) {
|
|
190
|
+
const feature = resolveFeature(explicitFeature);
|
|
165
191
|
if (!feature)
|
|
166
|
-
return "Error: No
|
|
192
|
+
return "Error: No feature specified. Create a feature or provide feature param.";
|
|
193
|
+
captureSession(feature, toolContext);
|
|
167
194
|
const comments = planService.getComments(feature);
|
|
168
195
|
if (comments.length > 0) {
|
|
169
196
|
return `Error: Cannot approve - ${comments.length} unresolved comment(s). Address them first.`;
|
|
@@ -174,11 +201,13 @@ const plugin = async (ctx) => {
|
|
|
174
201
|
}),
|
|
175
202
|
hive_tasks_sync: tool({
|
|
176
203
|
description: 'Generate tasks from approved plan',
|
|
177
|
-
args: {
|
|
178
|
-
|
|
179
|
-
|
|
204
|
+
args: {
|
|
205
|
+
feature: tool.schema.string().optional().describe('Feature name (defaults to detection or single feature)'),
|
|
206
|
+
},
|
|
207
|
+
async execute({ feature: explicitFeature }) {
|
|
208
|
+
const feature = resolveFeature(explicitFeature);
|
|
180
209
|
if (!feature)
|
|
181
|
-
return "Error: No
|
|
210
|
+
return "Error: No feature specified. Create a feature or provide feature param.";
|
|
182
211
|
const featureData = featureService.get(feature);
|
|
183
212
|
if (!featureData || featureData.status === 'planning') {
|
|
184
213
|
return "Error: Plan must be approved first";
|
|
@@ -195,11 +224,12 @@ const plugin = async (ctx) => {
|
|
|
195
224
|
args: {
|
|
196
225
|
name: tool.schema.string().describe('Task name'),
|
|
197
226
|
order: tool.schema.number().optional().describe('Task order'),
|
|
227
|
+
feature: tool.schema.string().optional().describe('Feature name (defaults to detection or single feature)'),
|
|
198
228
|
},
|
|
199
|
-
async execute({ name, order }) {
|
|
200
|
-
const feature =
|
|
229
|
+
async execute({ name, order, feature: explicitFeature }) {
|
|
230
|
+
const feature = resolveFeature(explicitFeature);
|
|
201
231
|
if (!feature)
|
|
202
|
-
return "Error: No
|
|
232
|
+
return "Error: No feature specified. Create a feature or provide feature param.";
|
|
203
233
|
const folder = taskService.create(feature, name, order);
|
|
204
234
|
return `Manual task created: ${folder}`;
|
|
205
235
|
},
|
|
@@ -210,11 +240,12 @@ const plugin = async (ctx) => {
|
|
|
210
240
|
task: tool.schema.string().describe('Task folder name'),
|
|
211
241
|
status: tool.schema.string().optional().describe('New status: pending, in_progress, done, cancelled'),
|
|
212
242
|
summary: tool.schema.string().optional().describe('Summary of work'),
|
|
243
|
+
feature: tool.schema.string().optional().describe('Feature name (defaults to detection or single feature)'),
|
|
213
244
|
},
|
|
214
|
-
async execute({ task, status, summary }) {
|
|
215
|
-
const feature =
|
|
245
|
+
async execute({ task, status, summary, feature: explicitFeature }) {
|
|
246
|
+
const feature = resolveFeature(explicitFeature);
|
|
216
247
|
if (!feature)
|
|
217
|
-
return "Error: No
|
|
248
|
+
return "Error: No feature specified. Create a feature or provide feature param.";
|
|
218
249
|
const updated = taskService.update(feature, task, {
|
|
219
250
|
status: status,
|
|
220
251
|
summary,
|
|
@@ -224,19 +255,47 @@ const plugin = async (ctx) => {
|
|
|
224
255
|
}),
|
|
225
256
|
hive_exec_start: tool({
|
|
226
257
|
description: 'Create worktree and begin work on task',
|
|
227
|
-
args: {
|
|
228
|
-
|
|
229
|
-
|
|
258
|
+
args: {
|
|
259
|
+
task: tool.schema.string().describe('Task folder name'),
|
|
260
|
+
feature: tool.schema.string().optional().describe('Feature name (defaults to detection or single feature)'),
|
|
261
|
+
},
|
|
262
|
+
async execute({ task, feature: explicitFeature }) {
|
|
263
|
+
const feature = resolveFeature(explicitFeature);
|
|
230
264
|
if (!feature)
|
|
231
|
-
return "Error: No
|
|
265
|
+
return "Error: No feature specified. Create a feature or provide feature param.";
|
|
232
266
|
const taskInfo = taskService.get(feature, task);
|
|
233
267
|
if (!taskInfo)
|
|
234
268
|
return `Error: Task "${task}" not found`;
|
|
235
269
|
if (taskInfo.status === 'done')
|
|
236
270
|
return "Error: Task already completed";
|
|
237
271
|
const worktree = await worktreeService.create(feature, task);
|
|
238
|
-
taskService.update(feature, task, {
|
|
239
|
-
|
|
272
|
+
taskService.update(feature, task, {
|
|
273
|
+
status: 'in_progress',
|
|
274
|
+
baseCommit: worktree.commit,
|
|
275
|
+
});
|
|
276
|
+
// Generate spec.md with context for task
|
|
277
|
+
const planResult = planService.read(feature);
|
|
278
|
+
const contextCompiled = contextService.compile(feature);
|
|
279
|
+
const allTasks = taskService.list(feature);
|
|
280
|
+
const priorTasks = allTasks
|
|
281
|
+
.filter(t => t.status === 'done')
|
|
282
|
+
.map(t => `- ${t.folder}: ${t.summary || 'No summary'}`);
|
|
283
|
+
let specContent = `# Task: ${task}\n\n`;
|
|
284
|
+
specContent += `## Feature: ${feature}\n\n`;
|
|
285
|
+
if (planResult) {
|
|
286
|
+
const taskMatch = planResult.content.match(new RegExp(`###\\s*\\d+\\.\\s*${taskInfo.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?(?=###|$)`, 'i'));
|
|
287
|
+
if (taskMatch) {
|
|
288
|
+
specContent += `## Plan Section\n\n${taskMatch[0].trim()}\n\n`;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (contextCompiled) {
|
|
292
|
+
specContent += `## Context\n\n${contextCompiled}\n\n`;
|
|
293
|
+
}
|
|
294
|
+
if (priorTasks.length > 0) {
|
|
295
|
+
specContent += `## Completed Tasks\n\n${priorTasks.join('\n')}\n\n`;
|
|
296
|
+
}
|
|
297
|
+
taskService.writeSpec(feature, task, specContent);
|
|
298
|
+
return `Worktree created at ${worktree.path}\nBranch: ${worktree.branch}\nBase commit: ${worktree.commit}\nSpec: ${task}/spec.md generated`;
|
|
240
299
|
},
|
|
241
300
|
}),
|
|
242
301
|
hive_exec_complete: tool({
|
|
@@ -244,39 +303,191 @@ const plugin = async (ctx) => {
|
|
|
244
303
|
args: {
|
|
245
304
|
task: tool.schema.string().describe('Task folder name'),
|
|
246
305
|
summary: tool.schema.string().describe('Summary of what was done'),
|
|
306
|
+
feature: tool.schema.string().optional().describe('Feature name (defaults to detection or single feature)'),
|
|
247
307
|
},
|
|
248
|
-
async execute({ task, summary }) {
|
|
249
|
-
const feature =
|
|
308
|
+
async execute({ task, summary, feature: explicitFeature }) {
|
|
309
|
+
const feature = resolveFeature(explicitFeature);
|
|
250
310
|
if (!feature)
|
|
251
|
-
return "Error: No
|
|
311
|
+
return "Error: No feature specified. Create a feature or provide feature param.";
|
|
252
312
|
const taskInfo = taskService.get(feature, task);
|
|
253
313
|
if (!taskInfo)
|
|
254
314
|
return `Error: Task "${task}" not found`;
|
|
255
315
|
if (taskInfo.status !== 'in_progress')
|
|
256
316
|
return "Error: Task not in progress";
|
|
257
317
|
const diff = await worktreeService.getDiff(feature, task);
|
|
318
|
+
let changesApplied = false;
|
|
319
|
+
let applyError = "";
|
|
320
|
+
if (diff?.hasDiff) {
|
|
321
|
+
const result = await worktreeService.applyDiff(feature, task);
|
|
322
|
+
changesApplied = result.success;
|
|
323
|
+
if (!result.success) {
|
|
324
|
+
applyError = result.error || "Unknown apply error";
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const reportLines = [
|
|
328
|
+
`# Task Report: ${task}`,
|
|
329
|
+
'',
|
|
330
|
+
`**Feature:** ${feature}`,
|
|
331
|
+
`**Completed:** ${new Date().toISOString()}`,
|
|
332
|
+
`**Status:** ${applyError ? 'completed with errors' : 'success'}`,
|
|
333
|
+
'',
|
|
334
|
+
'---',
|
|
335
|
+
'',
|
|
336
|
+
'## Summary',
|
|
337
|
+
'',
|
|
338
|
+
summary,
|
|
339
|
+
'',
|
|
340
|
+
];
|
|
258
341
|
if (diff?.hasDiff) {
|
|
259
|
-
|
|
342
|
+
reportLines.push('---', '', '## Changes', '', `- **Files changed:** ${diff.filesChanged.length}`, `- **Insertions:** +${diff.insertions}`, `- **Deletions:** -${diff.deletions}`, '');
|
|
343
|
+
if (diff.filesChanged.length > 0) {
|
|
344
|
+
reportLines.push('### Files Modified', '');
|
|
345
|
+
for (const file of diff.filesChanged) {
|
|
346
|
+
reportLines.push(`- \`${file}\``);
|
|
347
|
+
}
|
|
348
|
+
reportLines.push('');
|
|
349
|
+
}
|
|
260
350
|
}
|
|
261
|
-
|
|
262
|
-
|
|
351
|
+
else {
|
|
352
|
+
reportLines.push('---', '', '## Changes', '', '_No file changes detected_', '');
|
|
353
|
+
}
|
|
354
|
+
taskService.writeReport(feature, task, reportLines.join('\n'));
|
|
263
355
|
taskService.update(feature, task, { status: 'done', summary });
|
|
264
356
|
await worktreeService.remove(feature, task);
|
|
265
|
-
|
|
357
|
+
if (applyError) {
|
|
358
|
+
return `Task "${task}" completed but changes failed to apply: ${applyError}`;
|
|
359
|
+
}
|
|
360
|
+
return `Task "${task}" completed.${changesApplied ? " Changes applied." : ""}`;
|
|
266
361
|
},
|
|
267
362
|
}),
|
|
268
363
|
hive_exec_abort: tool({
|
|
269
364
|
description: 'Abort task: discard changes, reset status',
|
|
270
|
-
args: {
|
|
271
|
-
|
|
272
|
-
|
|
365
|
+
args: {
|
|
366
|
+
task: tool.schema.string().describe('Task folder name'),
|
|
367
|
+
feature: tool.schema.string().optional().describe('Feature name (defaults to detection or single feature)'),
|
|
368
|
+
},
|
|
369
|
+
async execute({ task, feature: explicitFeature }) {
|
|
370
|
+
const feature = resolveFeature(explicitFeature);
|
|
273
371
|
if (!feature)
|
|
274
|
-
return "Error: No
|
|
372
|
+
return "Error: No feature specified. Create a feature or provide feature param.";
|
|
275
373
|
await worktreeService.remove(feature, task);
|
|
276
374
|
taskService.update(feature, task, { status: 'pending' });
|
|
277
375
|
return `Task "${task}" aborted. Status reset to pending.`;
|
|
278
376
|
},
|
|
279
377
|
}),
|
|
378
|
+
// Context Tools
|
|
379
|
+
hive_context_write: tool({
|
|
380
|
+
description: 'Write a context file for the feature. Context files store persistent notes, decisions, and reference material.',
|
|
381
|
+
args: {
|
|
382
|
+
name: tool.schema.string().describe('Context file name (e.g., "decisions", "architecture", "notes")'),
|
|
383
|
+
content: tool.schema.string().describe('Markdown content to write'),
|
|
384
|
+
feature: tool.schema.string().optional().describe('Feature name (defaults to active)'),
|
|
385
|
+
},
|
|
386
|
+
async execute({ name, content, feature: explicitFeature }) {
|
|
387
|
+
const feature = resolveFeature(explicitFeature);
|
|
388
|
+
if (!feature)
|
|
389
|
+
return "Error: No feature specified. Create a feature or provide feature param.";
|
|
390
|
+
const filePath = contextService.write(feature, name, content);
|
|
391
|
+
return `Context file written: ${filePath}`;
|
|
392
|
+
},
|
|
393
|
+
}),
|
|
394
|
+
hive_context_read: tool({
|
|
395
|
+
description: 'Read a specific context file or all context for the feature',
|
|
396
|
+
args: {
|
|
397
|
+
name: tool.schema.string().optional().describe('Context file name. If omitted, returns all context compiled.'),
|
|
398
|
+
feature: tool.schema.string().optional().describe('Feature name (defaults to active)'),
|
|
399
|
+
},
|
|
400
|
+
async execute({ name, feature: explicitFeature }) {
|
|
401
|
+
const feature = resolveFeature(explicitFeature);
|
|
402
|
+
if (!feature)
|
|
403
|
+
return "Error: No feature specified. Create a feature or provide feature param.";
|
|
404
|
+
if (name) {
|
|
405
|
+
const content = contextService.read(feature, name);
|
|
406
|
+
if (!content)
|
|
407
|
+
return `Error: Context file '${name}' not found`;
|
|
408
|
+
return content;
|
|
409
|
+
}
|
|
410
|
+
const compiled = contextService.compile(feature);
|
|
411
|
+
if (!compiled)
|
|
412
|
+
return "No context files found";
|
|
413
|
+
return compiled;
|
|
414
|
+
},
|
|
415
|
+
}),
|
|
416
|
+
hive_context_list: tool({
|
|
417
|
+
description: 'List all context files for the feature',
|
|
418
|
+
args: {
|
|
419
|
+
feature: tool.schema.string().optional().describe('Feature name (defaults to active)'),
|
|
420
|
+
},
|
|
421
|
+
async execute({ feature: explicitFeature }) {
|
|
422
|
+
const feature = resolveFeature(explicitFeature);
|
|
423
|
+
if (!feature)
|
|
424
|
+
return "Error: No feature specified. Create a feature or provide feature param.";
|
|
425
|
+
const files = contextService.list(feature);
|
|
426
|
+
if (files.length === 0)
|
|
427
|
+
return "No context files";
|
|
428
|
+
return files.map(f => `${f.name} (${f.content.length} chars, updated ${f.updatedAt})`).join('\n');
|
|
429
|
+
},
|
|
430
|
+
}),
|
|
431
|
+
// Session Tools
|
|
432
|
+
hive_session_open: tool({
|
|
433
|
+
description: 'Open session, return full context for a feature',
|
|
434
|
+
args: {
|
|
435
|
+
feature: tool.schema.string().optional().describe('Feature name (defaults to active)'),
|
|
436
|
+
task: tool.schema.string().optional().describe('Task folder to focus on'),
|
|
437
|
+
},
|
|
438
|
+
async execute({ feature: explicitFeature, task }, toolContext) {
|
|
439
|
+
const feature = resolveFeature(explicitFeature);
|
|
440
|
+
if (!feature)
|
|
441
|
+
return "Error: No feature specified. Create a feature or provide feature param.";
|
|
442
|
+
const featureData = featureService.get(feature);
|
|
443
|
+
if (!featureData)
|
|
444
|
+
return `Error: Feature '${feature}' not found`;
|
|
445
|
+
// Track session
|
|
446
|
+
const ctx = toolContext;
|
|
447
|
+
if (ctx?.sessionID) {
|
|
448
|
+
sessionService.track(feature, ctx.sessionID, task);
|
|
449
|
+
}
|
|
450
|
+
const planResult = planService.read(feature);
|
|
451
|
+
const tasks = taskService.list(feature);
|
|
452
|
+
const contextCompiled = contextService.compile(feature);
|
|
453
|
+
const sessions = sessionService.list(feature);
|
|
454
|
+
let output = `## Feature: ${feature} [${featureData.status}]\n\n`;
|
|
455
|
+
if (planResult) {
|
|
456
|
+
output += `### Plan\n${planResult.content.substring(0, 500)}...\n\n`;
|
|
457
|
+
}
|
|
458
|
+
output += `### Tasks (${tasks.length})\n`;
|
|
459
|
+
tasks.forEach(t => {
|
|
460
|
+
output += `- ${t.folder}: ${t.name} [${t.status}]\n`;
|
|
461
|
+
});
|
|
462
|
+
if (contextCompiled) {
|
|
463
|
+
output += `\n### Context\n${contextCompiled.substring(0, 500)}...\n`;
|
|
464
|
+
}
|
|
465
|
+
output += `\n### Sessions (${sessions.length})\n`;
|
|
466
|
+
sessions.forEach(s => {
|
|
467
|
+
output += `- ${s.sessionId} (${s.taskFolder || 'no task'})\n`;
|
|
468
|
+
});
|
|
469
|
+
return output;
|
|
470
|
+
},
|
|
471
|
+
}),
|
|
472
|
+
hive_session_list: tool({
|
|
473
|
+
description: 'List all sessions for the feature',
|
|
474
|
+
args: {
|
|
475
|
+
feature: tool.schema.string().optional().describe('Feature name (defaults to active)'),
|
|
476
|
+
},
|
|
477
|
+
async execute({ feature: explicitFeature }) {
|
|
478
|
+
const feature = resolveFeature(explicitFeature);
|
|
479
|
+
if (!feature)
|
|
480
|
+
return "Error: No feature specified. Create a feature or provide feature param.";
|
|
481
|
+
const sessions = sessionService.list(feature);
|
|
482
|
+
const master = sessionService.getMaster(feature);
|
|
483
|
+
if (sessions.length === 0)
|
|
484
|
+
return "No sessions";
|
|
485
|
+
return sessions.map(s => {
|
|
486
|
+
const masterMark = s.sessionId === master ? ' (master)' : '';
|
|
487
|
+
return `${s.sessionId}${masterMark} - ${s.taskFolder || 'no task'} - ${s.lastActiveAt}`;
|
|
488
|
+
}).join('\n');
|
|
489
|
+
},
|
|
490
|
+
}),
|
|
280
491
|
},
|
|
281
492
|
command: {
|
|
282
493
|
hive: {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ContextFile {
|
|
2
|
+
name: string;
|
|
3
|
+
content: string;
|
|
4
|
+
updatedAt: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class ContextService {
|
|
7
|
+
private projectRoot;
|
|
8
|
+
constructor(projectRoot: string);
|
|
9
|
+
write(featureName: string, fileName: string, content: string): string;
|
|
10
|
+
read(featureName: string, fileName: string): string | null;
|
|
11
|
+
list(featureName: string): ContextFile[];
|
|
12
|
+
delete(featureName: string, fileName: string): boolean;
|
|
13
|
+
compile(featureName: string): string;
|
|
14
|
+
private normalizeFileName;
|
|
15
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { getContextPath, ensureDir, fileExists, readText, writeText } from '../utils/paths.js';
|
|
4
|
+
export class ContextService {
|
|
5
|
+
projectRoot;
|
|
6
|
+
constructor(projectRoot) {
|
|
7
|
+
this.projectRoot = projectRoot;
|
|
8
|
+
}
|
|
9
|
+
write(featureName, fileName, content) {
|
|
10
|
+
const contextPath = getContextPath(this.projectRoot, featureName);
|
|
11
|
+
ensureDir(contextPath);
|
|
12
|
+
const filePath = path.join(contextPath, this.normalizeFileName(fileName));
|
|
13
|
+
writeText(filePath, content);
|
|
14
|
+
return filePath;
|
|
15
|
+
}
|
|
16
|
+
read(featureName, fileName) {
|
|
17
|
+
const contextPath = getContextPath(this.projectRoot, featureName);
|
|
18
|
+
const filePath = path.join(contextPath, this.normalizeFileName(fileName));
|
|
19
|
+
return readText(filePath);
|
|
20
|
+
}
|
|
21
|
+
list(featureName) {
|
|
22
|
+
const contextPath = getContextPath(this.projectRoot, featureName);
|
|
23
|
+
if (!fileExists(contextPath))
|
|
24
|
+
return [];
|
|
25
|
+
const files = fs.readdirSync(contextPath, { withFileTypes: true })
|
|
26
|
+
.filter(f => f.isFile() && f.name.endsWith('.md'))
|
|
27
|
+
.map(f => f.name);
|
|
28
|
+
return files.map(name => {
|
|
29
|
+
const filePath = path.join(contextPath, name);
|
|
30
|
+
const stat = fs.statSync(filePath);
|
|
31
|
+
const content = readText(filePath) || '';
|
|
32
|
+
return {
|
|
33
|
+
name: name.replace(/\.md$/, ''),
|
|
34
|
+
content,
|
|
35
|
+
updatedAt: stat.mtime.toISOString(),
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
delete(featureName, fileName) {
|
|
40
|
+
const contextPath = getContextPath(this.projectRoot, featureName);
|
|
41
|
+
const filePath = path.join(contextPath, this.normalizeFileName(fileName));
|
|
42
|
+
if (fileExists(filePath)) {
|
|
43
|
+
fs.unlinkSync(filePath);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
compile(featureName) {
|
|
49
|
+
const files = this.list(featureName);
|
|
50
|
+
if (files.length === 0)
|
|
51
|
+
return '';
|
|
52
|
+
const sections = files.map(f => `## ${f.name}\n\n${f.content}`);
|
|
53
|
+
return sections.join('\n\n---\n\n');
|
|
54
|
+
}
|
|
55
|
+
normalizeFileName(name) {
|
|
56
|
+
const normalized = name.replace(/\.md$/, '');
|
|
57
|
+
return `${normalized}.md`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -5,8 +5,6 @@ export declare class FeatureService {
|
|
|
5
5
|
create(name: string, ticket?: string): FeatureJson;
|
|
6
6
|
get(name: string): FeatureJson | null;
|
|
7
7
|
list(): string[];
|
|
8
|
-
getActive(): string | null;
|
|
9
|
-
setActive(name: string): void;
|
|
10
8
|
updateStatus(name: string, status: FeatureStatusType): FeatureJson;
|
|
11
9
|
getInfo(name: string): FeatureInfo | null;
|
|
12
10
|
private getTasks;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
|
-
import { getFeaturePath, getFeaturesPath, getFeatureJsonPath, getContextPath, getTasksPath,
|
|
2
|
+
import { getFeaturePath, getFeaturesPath, getFeatureJsonPath, getContextPath, getTasksPath, getPlanPath, getCommentsPath, ensureDir, readJson, writeJson, fileExists, } from '../utils/paths.js';
|
|
3
3
|
export class FeatureService {
|
|
4
4
|
projectRoot;
|
|
5
5
|
constructor(projectRoot) {
|
|
@@ -20,7 +20,6 @@ export class FeatureService {
|
|
|
20
20
|
createdAt: new Date().toISOString(),
|
|
21
21
|
};
|
|
22
22
|
writeJson(getFeatureJsonPath(this.projectRoot, name), feature);
|
|
23
|
-
this.setActive(name);
|
|
24
23
|
return feature;
|
|
25
24
|
}
|
|
26
25
|
get(name) {
|
|
@@ -34,15 +33,6 @@ export class FeatureService {
|
|
|
34
33
|
.filter(d => d.isDirectory())
|
|
35
34
|
.map(d => d.name);
|
|
36
35
|
}
|
|
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
36
|
updateStatus(name, status) {
|
|
47
37
|
const feature = this.get(name);
|
|
48
38
|
if (!feature)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|