neohive 6.0.2 → 6.1.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.
@@ -0,0 +1,286 @@
1
+ 'use strict';
2
+
3
+ // Workflow tools: create, advance, status.
4
+ // Extracted from server.js as part of modular tool architecture.
5
+
6
+ const fs = require('fs');
7
+
8
+ module.exports = function (ctx) {
9
+ const { state, helpers, files } = ctx;
10
+
11
+ const {
12
+ getWorkflows, saveWorkflows, saveWorkflowCheckpoint, findReadySteps,
13
+ getAgents, isPidAlive, getTasks, saveTasks, generateId, ensureDataDir,
14
+ broadcastSystemMessage, sendSystemMessage, touchActivity, appendNotification,
15
+ getMessagesFile, getHistoryFile, canSendTo, generateCompletionReport,
16
+ } = helpers;
17
+
18
+ // --- Create Workflow ---
19
+
20
+ function toolCreateWorkflow(name, steps, autonomous, parallel) {
21
+ if (!state.registeredName) return { error: 'You must call register() first' };
22
+ autonomous = !!autonomous;
23
+ parallel = !!parallel;
24
+ if (!name || typeof name !== 'string' || name.length > 50) return { error: 'name must be 1-50 chars' };
25
+ if (!Array.isArray(steps) || steps.length < 2 || steps.length > 30) return { error: 'steps must be array of 2-30 items' };
26
+
27
+ const agents = getAgents();
28
+ const workflows = getWorkflows();
29
+ const workflowId = 'wf_' + generateId();
30
+
31
+ const parsedSteps = steps.map((s, i) => {
32
+ const step = typeof s === 'string' ? { description: s } : s;
33
+ if (!step.description) return null;
34
+ return {
35
+ id: i + 1,
36
+ description: step.description.substring(0, 200),
37
+ assignee: step.assignee || null,
38
+ depends_on: Array.isArray(step.depends_on) ? step.depends_on : [],
39
+ requires_approval: !!step.requires_approval,
40
+ status: 'pending',
41
+ started_at: null,
42
+ completed_at: null,
43
+ notes: '',
44
+ };
45
+ });
46
+ if (parsedSteps.includes(null)) return { error: 'Each step must have a description' };
47
+
48
+ const stepIds = parsedSteps.map(s => s.id);
49
+ for (const step of parsedSteps) {
50
+ for (const depId of step.depends_on) {
51
+ if (!stepIds.includes(depId)) return { error: `Step ${step.id} depends_on non-existent step ${depId}` };
52
+ if (depId >= step.id) return { error: `Step ${step.id} cannot depend on step ${depId} (must depend on earlier steps)` };
53
+ }
54
+ }
55
+
56
+ const readySteps = parsedSteps.filter(s => s.depends_on.length === 0);
57
+ if (parallel) {
58
+ for (const s of readySteps) {
59
+ s.status = 'in_progress';
60
+ s.started_at = new Date().toISOString();
61
+ }
62
+ } else {
63
+ readySteps[0].status = 'in_progress';
64
+ readySteps[0].started_at = new Date().toISOString();
65
+ }
66
+
67
+ const workflow = {
68
+ id: workflowId,
69
+ name,
70
+ steps: parsedSteps,
71
+ status: 'active',
72
+ autonomous,
73
+ parallel,
74
+ created_by: state.registeredName,
75
+ created_at: new Date().toISOString(),
76
+ updated_at: new Date().toISOString(),
77
+ };
78
+
79
+ if (workflows.length >= 500) return { error: 'Workflow limit reached (max 500).' };
80
+ workflows.push(workflow);
81
+ ensureDataDir();
82
+ saveWorkflows(workflows);
83
+
84
+ const startedSteps = parsedSteps.filter(s => s.status === 'in_progress');
85
+ for (const step of startedSteps) {
86
+ if (step.assignee && agents[step.assignee] && step.assignee !== state.registeredName) {
87
+ const handoffContent = `[Workflow "${name}"] Step ${step.id} assigned to you: ${step.description}` +
88
+ (autonomous ? '\n\nThis is an AUTONOMOUS workflow. Call get_work() to enter the proactive work loop. Do NOT wait for approval.' : '');
89
+ state.messageSeq++;
90
+ const msg = { id: generateId(), seq: state.messageSeq, from: state.registeredName, to: step.assignee, content: handoffContent, timestamp: new Date().toISOString(), type: 'handoff' };
91
+ fs.appendFileSync(getMessagesFile(state.currentBranch), JSON.stringify(msg) + '\n');
92
+ fs.appendFileSync(getHistoryFile(state.currentBranch), JSON.stringify(msg) + '\n');
93
+ }
94
+ }
95
+ touchActivity();
96
+
97
+ return {
98
+ success: true,
99
+ workflow_id: workflowId,
100
+ name,
101
+ step_count: parsedSteps.length,
102
+ autonomous,
103
+ parallel,
104
+ started_steps: startedSteps.map(s => ({ id: s.id, description: s.description, assignee: s.assignee })),
105
+ message: autonomous ? 'Autonomous workflow created. All agents should call get_work() to enter the proactive work loop.' : undefined,
106
+ };
107
+ }
108
+
109
+ // --- Advance Workflow ---
110
+
111
+ function toolAdvanceWorkflow(workflowId, notes) {
112
+ if (!state.registeredName) return { error: 'You must call register() first' };
113
+
114
+ const workflows = getWorkflows();
115
+ const wf = workflows.find(w => w.id === workflowId);
116
+ if (!wf) return { error: `Workflow not found: ${workflowId}` };
117
+ if (wf.status !== 'active') return { error: 'Workflow is not active' };
118
+
119
+ const currentStep = wf.steps.find(s => s.status === 'in_progress');
120
+ if (!currentStep) return { error: 'No step currently in progress' };
121
+
122
+ currentStep.status = 'done';
123
+ currentStep.completed_at = new Date().toISOString();
124
+ if (notes) currentStep.notes = notes.substring(0, 500);
125
+
126
+ saveWorkflowCheckpoint(wf, currentStep);
127
+
128
+ // Auto-sync: mark matching in_progress tasks as done
129
+ try {
130
+ const tasks = getTasks();
131
+ const matchingTask = tasks.find(t =>
132
+ t.status === 'in_progress' && t.assignee === state.registeredName
133
+ );
134
+ if (matchingTask) {
135
+ matchingTask.status = 'done';
136
+ matchingTask.updated_at = new Date().toISOString();
137
+ matchingTask.notes.push({ by: '__system__', text: `Auto-completed via workflow step "${currentStep.description}"`, at: new Date().toISOString() });
138
+ saveTasks(tasks);
139
+ }
140
+ } catch (e) { /* auto-complete task on workflow advance failed */ }
141
+
142
+ const nextSteps = findReadySteps(wf);
143
+ if (nextSteps.length > 0) {
144
+ const agents = getAgents();
145
+ for (const step of nextSteps) {
146
+ if (step.requires_approval) {
147
+ step.status = 'awaiting_approval';
148
+ step.approval_requested_at = new Date().toISOString();
149
+ sendSystemMessage('__user__',
150
+ `[APPROVAL NEEDED] Workflow "${wf.name}" — Step ${step.id}: "${step.description}". Approve or reject from the dashboard.`
151
+ );
152
+ continue;
153
+ }
154
+ step.status = 'in_progress';
155
+ step.started_at = new Date().toISOString();
156
+ if (step.assignee && agents[step.assignee] && step.assignee !== state.registeredName && canSendTo(state.registeredName, step.assignee)) {
157
+ const handoffContent = `[Workflow "${wf.name}"] Step ${step.id} assigned to you: ${step.description}`;
158
+ state.messageSeq++;
159
+ const msg = { id: generateId(), seq: state.messageSeq, from: state.registeredName, to: step.assignee, content: handoffContent, timestamp: new Date().toISOString(), type: 'handoff' };
160
+ fs.appendFileSync(getMessagesFile(state.currentBranch), JSON.stringify(msg) + '\n');
161
+ fs.appendFileSync(getHistoryFile(state.currentBranch), JSON.stringify(msg) + '\n');
162
+ }
163
+ }
164
+ } else if (wf.steps.every(s => s.status === 'done')) {
165
+ wf.status = 'completed';
166
+ }
167
+ wf.updated_at = new Date().toISOString();
168
+ saveWorkflows(workflows);
169
+ touchActivity();
170
+
171
+ const doneCount = wf.steps.filter(s => s.status === 'done').length;
172
+ const pct = Math.round((doneCount / wf.steps.length) * 100);
173
+ appendNotification('workflow_advanced', state.registeredName, `Workflow "${wf.name}" step ${currentStep.id} done (${pct}%)`, wf.id);
174
+
175
+ return {
176
+ success: true,
177
+ workflow_id: wf.id,
178
+ completed_step: currentStep.id,
179
+ next_steps: nextSteps.length > 0 ? nextSteps.map(s => ({ id: s.id, description: s.description, assignee: s.assignee })) : null,
180
+ progress: `${doneCount}/${wf.steps.length} (${pct}%)`,
181
+ workflow_status: wf.status,
182
+ };
183
+ }
184
+
185
+ // --- Workflow Status ---
186
+
187
+ function toolWorkflowStatus(workflowId, action, checkpointIndex) {
188
+ const workflows = getWorkflows();
189
+
190
+ if (action === 'rollback' && workflowId && checkpointIndex !== undefined) {
191
+ const wf = workflows.find(w => w.id === workflowId);
192
+ if (!wf) return { error: `Workflow not found: ${workflowId}` };
193
+ if (!wf.checkpoints || !wf.checkpoints[checkpointIndex]) return { error: 'Checkpoint not found' };
194
+ const checkpoint = wf.checkpoints[checkpointIndex];
195
+ for (const savedStep of checkpoint.step_states) {
196
+ const step = wf.steps.find(s => s.id === savedStep.id);
197
+ if (step) { step.status = savedStep.status; step.assignee = savedStep.assignee; }
198
+ }
199
+ wf.updated_at = new Date().toISOString();
200
+ saveWorkflows(workflows);
201
+ broadcastSystemMessage(`[WORKFLOW] Rolled back "${wf.name}" to checkpoint: step "${checkpoint.step_description}"`);
202
+ return { success: true, rolled_back_to: checkpoint };
203
+ }
204
+
205
+ if (workflowId) {
206
+ const wf = workflows.find(w => w.id === workflowId);
207
+ if (!wf) return { error: `Workflow not found: ${workflowId}` };
208
+ const doneCount = wf.steps.filter(s => s.status === 'done').length;
209
+ const pct = Math.round((doneCount / wf.steps.length) * 100);
210
+ const result = { workflow: wf, progress: `${doneCount}/${wf.steps.length} (${pct}%)` };
211
+ if (wf.checkpoints) result.checkpoints = wf.checkpoints.length;
212
+ if (wf.status === 'completed') result.report = generateCompletionReport(wf);
213
+ return result;
214
+ }
215
+ return {
216
+ count: workflows.length,
217
+ workflows: workflows.map(w => {
218
+ const doneCount = w.steps.filter(s => s.status === 'done').length;
219
+ return { id: w.id, name: w.name, status: w.status, steps: w.steps.length, done: doneCount, progress: Math.round((doneCount / w.steps.length) * 100) + '%', checkpoints: w.checkpoints ? w.checkpoints.length : 0 };
220
+ }),
221
+ };
222
+ }
223
+
224
+ // --- MCP tool definitions ---
225
+
226
+ const definitions = [
227
+ {
228
+ name: 'create_workflow',
229
+ description: 'Create a multi-step workflow pipeline. Each step can have a description, assignee, and depends_on (step IDs). Set autonomous=true for proactive work loop (agents auto-advance, no human gates). Set parallel=true to run independent steps simultaneously.',
230
+ inputSchema: {
231
+ type: 'object',
232
+ properties: {
233
+ name: { type: 'string', description: 'Workflow name (max 50 chars)' },
234
+ steps: {
235
+ type: 'array',
236
+ description: 'Array of steps. Each step is a string (description) or {description, assignee, depends_on: [stepIds]}.',
237
+ items: {
238
+ oneOf: [
239
+ { type: 'string' },
240
+ { type: 'object', properties: { description: { type: 'string' }, assignee: { type: 'string' }, depends_on: { type: 'array', items: { type: 'number' }, description: 'Step IDs this step depends on (must complete first)' } }, required: ['description'] },
241
+ ],
242
+ },
243
+ },
244
+ autonomous: { type: 'boolean', default: false, description: 'If true, agents auto-advance through steps without waiting for approval. Enables proactive work loop, relaxed send limits, fast cooldowns, and 30s listen cap.' },
245
+ parallel: { type: 'boolean', default: false, description: 'If true, steps with met dependencies run in parallel (multiple agents work simultaneously)' },
246
+ },
247
+ required: ['name', 'steps'],
248
+ additionalProperties: false,
249
+ },
250
+ },
251
+ {
252
+ name: 'advance_workflow',
253
+ description: 'Mark the current step as done and start the next step. Auto-sends a handoff message to the next assignee.',
254
+ inputSchema: {
255
+ type: 'object',
256
+ properties: {
257
+ workflow_id: { type: 'string', description: 'Workflow ID' },
258
+ notes: { type: 'string', description: 'Optional completion notes (max 500 chars)' },
259
+ },
260
+ required: ['workflow_id'],
261
+ additionalProperties: false,
262
+ },
263
+ },
264
+ {
265
+ name: 'workflow_status',
266
+ description: 'Get status of a specific workflow or all workflows. Shows step progress, checkpoints, and completion percentage. Use action="rollback" to rollback to a checkpoint.',
267
+ inputSchema: {
268
+ type: 'object',
269
+ properties: {
270
+ workflow_id: { type: 'string', description: 'Workflow ID (optional — omit for all workflows)' },
271
+ action: { type: 'string', enum: ['status', 'rollback'], description: 'Action (default: status)' },
272
+ checkpoint_index: { type: 'number', description: 'Checkpoint index to rollback to (for rollback action)' },
273
+ },
274
+ additionalProperties: false,
275
+ },
276
+ },
277
+ ];
278
+
279
+ const handlers = {
280
+ create_workflow: function (args) { return toolCreateWorkflow(args.name, args.steps, args.autonomous, args.parallel); },
281
+ advance_workflow: function (args) { return toolAdvanceWorkflow(args.workflow_id, args.notes); },
282
+ workflow_status: function (args) { return toolWorkflowStatus(args.workflow_id, args.action, args.checkpoint_index); },
283
+ };
284
+
285
+ return { definitions, handlers };
286
+ };