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.
- 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 -75
- 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.d.ts +3 -9
- package/dist/tools/execTools.js +14 -12
- package/dist/tools/featureTools.d.ts +4 -48
- package/dist/tools/featureTools.js +11 -51
- package/dist/tools/planTools.d.ts +5 -15
- package/dist/tools/planTools.js +16 -16
- package/dist/tools/sessionTools.d.ts +35 -0
- package/dist/tools/sessionTools.js +95 -0
- package/dist/tools/taskTools.d.ts +6 -18
- package/dist/tools/taskTools.js +18 -19
- 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,58 +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
|
-
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
153
|
hive_plan_write: tool({
|
|
147
154
|
description: 'Write plan.md (clears existing comments)',
|
|
148
|
-
args: {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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);
|
|
152
161
|
if (!feature)
|
|
153
|
-
return "Error: No
|
|
162
|
+
return "Error: No feature specified. Create a feature or provide feature param.";
|
|
163
|
+
captureSession(feature, toolContext);
|
|
154
164
|
const planPath = planService.write(feature, content);
|
|
155
165
|
return `Plan written to ${planPath}. Comments cleared for fresh review.`;
|
|
156
166
|
},
|
|
157
167
|
}),
|
|
158
168
|
hive_plan_read: tool({
|
|
159
169
|
description: 'Read plan.md and user comments',
|
|
160
|
-
args: {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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);
|
|
164
175
|
if (!feature)
|
|
165
|
-
return "Error: No
|
|
176
|
+
return "Error: No feature specified. Create a feature or provide feature param.";
|
|
177
|
+
captureSession(feature, toolContext);
|
|
166
178
|
const result = planService.read(feature);
|
|
167
179
|
if (!result)
|
|
168
180
|
return "Error: No plan.md found";
|
|
@@ -171,12 +183,14 @@ const plugin = async (ctx) => {
|
|
|
171
183
|
}),
|
|
172
184
|
hive_plan_approve: tool({
|
|
173
185
|
description: 'Approve plan for execution',
|
|
174
|
-
args: {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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);
|
|
178
191
|
if (!feature)
|
|
179
|
-
return "Error: No
|
|
192
|
+
return "Error: No feature specified. Create a feature or provide feature param.";
|
|
193
|
+
captureSession(feature, toolContext);
|
|
180
194
|
const comments = planService.getComments(feature);
|
|
181
195
|
if (comments.length > 0) {
|
|
182
196
|
return `Error: Cannot approve - ${comments.length} unresolved comment(s). Address them first.`;
|
|
@@ -187,11 +201,13 @@ const plugin = async (ctx) => {
|
|
|
187
201
|
}),
|
|
188
202
|
hive_tasks_sync: tool({
|
|
189
203
|
description: 'Generate tasks from approved plan',
|
|
190
|
-
args: {
|
|
191
|
-
|
|
192
|
-
|
|
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);
|
|
193
209
|
if (!feature)
|
|
194
|
-
return "Error: No
|
|
210
|
+
return "Error: No feature specified. Create a feature or provide feature param.";
|
|
195
211
|
const featureData = featureService.get(feature);
|
|
196
212
|
if (!featureData || featureData.status === 'planning') {
|
|
197
213
|
return "Error: Plan must be approved first";
|
|
@@ -208,11 +224,12 @@ const plugin = async (ctx) => {
|
|
|
208
224
|
args: {
|
|
209
225
|
name: tool.schema.string().describe('Task name'),
|
|
210
226
|
order: tool.schema.number().optional().describe('Task order'),
|
|
227
|
+
feature: tool.schema.string().optional().describe('Feature name (defaults to detection or single feature)'),
|
|
211
228
|
},
|
|
212
|
-
async execute({ name, order }) {
|
|
213
|
-
const feature =
|
|
229
|
+
async execute({ name, order, feature: explicitFeature }) {
|
|
230
|
+
const feature = resolveFeature(explicitFeature);
|
|
214
231
|
if (!feature)
|
|
215
|
-
return "Error: No
|
|
232
|
+
return "Error: No feature specified. Create a feature or provide feature param.";
|
|
216
233
|
const folder = taskService.create(feature, name, order);
|
|
217
234
|
return `Manual task created: ${folder}`;
|
|
218
235
|
},
|
|
@@ -223,11 +240,12 @@ const plugin = async (ctx) => {
|
|
|
223
240
|
task: tool.schema.string().describe('Task folder name'),
|
|
224
241
|
status: tool.schema.string().optional().describe('New status: pending, in_progress, done, cancelled'),
|
|
225
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)'),
|
|
226
244
|
},
|
|
227
|
-
async execute({ task, status, summary }) {
|
|
228
|
-
const feature =
|
|
245
|
+
async execute({ task, status, summary, feature: explicitFeature }) {
|
|
246
|
+
const feature = resolveFeature(explicitFeature);
|
|
229
247
|
if (!feature)
|
|
230
|
-
return "Error: No
|
|
248
|
+
return "Error: No feature specified. Create a feature or provide feature param.";
|
|
231
249
|
const updated = taskService.update(feature, task, {
|
|
232
250
|
status: status,
|
|
233
251
|
summary,
|
|
@@ -237,19 +255,47 @@ const plugin = async (ctx) => {
|
|
|
237
255
|
}),
|
|
238
256
|
hive_exec_start: tool({
|
|
239
257
|
description: 'Create worktree and begin work on task',
|
|
240
|
-
args: {
|
|
241
|
-
|
|
242
|
-
|
|
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);
|
|
243
264
|
if (!feature)
|
|
244
|
-
return "Error: No
|
|
265
|
+
return "Error: No feature specified. Create a feature or provide feature param.";
|
|
245
266
|
const taskInfo = taskService.get(feature, task);
|
|
246
267
|
if (!taskInfo)
|
|
247
268
|
return `Error: Task "${task}" not found`;
|
|
248
269
|
if (taskInfo.status === 'done')
|
|
249
270
|
return "Error: Task already completed";
|
|
250
271
|
const worktree = await worktreeService.create(feature, task);
|
|
251
|
-
taskService.update(feature, task, {
|
|
252
|
-
|
|
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`;
|
|
253
299
|
},
|
|
254
300
|
}),
|
|
255
301
|
hive_exec_complete: tool({
|
|
@@ -257,39 +303,191 @@ const plugin = async (ctx) => {
|
|
|
257
303
|
args: {
|
|
258
304
|
task: tool.schema.string().describe('Task folder name'),
|
|
259
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)'),
|
|
260
307
|
},
|
|
261
|
-
async execute({ task, summary }) {
|
|
262
|
-
const feature =
|
|
308
|
+
async execute({ task, summary, feature: explicitFeature }) {
|
|
309
|
+
const feature = resolveFeature(explicitFeature);
|
|
263
310
|
if (!feature)
|
|
264
|
-
return "Error: No
|
|
311
|
+
return "Error: No feature specified. Create a feature or provide feature param.";
|
|
265
312
|
const taskInfo = taskService.get(feature, task);
|
|
266
313
|
if (!taskInfo)
|
|
267
314
|
return `Error: Task "${task}" not found`;
|
|
268
315
|
if (taskInfo.status !== 'in_progress')
|
|
269
316
|
return "Error: Task not in progress";
|
|
270
317
|
const diff = await worktreeService.getDiff(feature, task);
|
|
318
|
+
let changesApplied = false;
|
|
319
|
+
let applyError = "";
|
|
271
320
|
if (diff?.hasDiff) {
|
|
272
|
-
await worktreeService.applyDiff(feature, task);
|
|
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
|
+
];
|
|
341
|
+
if (diff?.hasDiff) {
|
|
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
|
+
}
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
reportLines.push('---', '', '## Changes', '', '_No file changes detected_', '');
|
|
273
353
|
}
|
|
274
|
-
|
|
275
|
-
taskService.writeReport(feature, task, report);
|
|
354
|
+
taskService.writeReport(feature, task, reportLines.join('\n'));
|
|
276
355
|
taskService.update(feature, task, { status: 'done', summary });
|
|
277
356
|
await worktreeService.remove(feature, task);
|
|
278
|
-
|
|
357
|
+
if (applyError) {
|
|
358
|
+
return `Task "${task}" completed but changes failed to apply: ${applyError}`;
|
|
359
|
+
}
|
|
360
|
+
return `Task "${task}" completed.${changesApplied ? " Changes applied." : ""}`;
|
|
279
361
|
},
|
|
280
362
|
}),
|
|
281
363
|
hive_exec_abort: tool({
|
|
282
364
|
description: 'Abort task: discard changes, reset status',
|
|
283
|
-
args: {
|
|
284
|
-
|
|
285
|
-
|
|
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);
|
|
286
371
|
if (!feature)
|
|
287
|
-
return "Error: No
|
|
372
|
+
return "Error: No feature specified. Create a feature or provide feature param.";
|
|
288
373
|
await worktreeService.remove(feature, task);
|
|
289
374
|
taskService.update(feature, task, { status: 'pending' });
|
|
290
375
|
return `Task "${task}" aborted. Status reset to pending.`;
|
|
291
376
|
},
|
|
292
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
|
+
}),
|
|
293
491
|
},
|
|
294
492
|
command: {
|
|
295
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 {};
|