neohive 6.2.2 → 6.4.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/CHANGELOG.md +26 -0
- package/README.md +3 -0
- package/cli.js +277 -55
- package/dashboard.html +108 -10
- package/dashboard.js +33 -2
- package/package.json +1 -1
- package/server.js +455 -508
- package/tools/governance.js +33 -8
- package/tools/knowledge.js +4 -4
- package/tools/messaging.js +3 -1
- package/tools/safety.js +4 -4
- package/tools/tasks.js +12 -7
- package/tools/workflows.js +2 -1
package/tools/governance.js
CHANGED
|
@@ -115,11 +115,13 @@ module.exports = function (ctx) {
|
|
|
115
115
|
reviews.push(review);
|
|
116
116
|
writeJsonFile(REVIEWS_FILE, reviews);
|
|
117
117
|
|
|
118
|
-
broadcastSystemMessage(`[REVIEW] ${state.registeredName} requests review of "${review.file}": ${review.description || 'No description'}.
|
|
118
|
+
broadcastSystemMessage(`[REVIEW REQUEST] ${state.registeredName} requests review of "${review.file}": ${review.description || 'No description'}. To review: (1) read the file "${review.file}", (2) call submit_review("${review.id}", "approved"/"changes_requested", "<your findings — min 50 chars>"). Feedback is required and must be substantive.`, state.registeredName);
|
|
119
119
|
touchActivity();
|
|
120
|
-
return { success: true, review_id: review.id, file: review.file,
|
|
120
|
+
return { success: true, review_id: review.id, file: review.file, next_action: 'Call listen() to wait for the review.' };
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
const REVIEW_FEEDBACK_MIN_LENGTH = 50;
|
|
124
|
+
|
|
123
125
|
function toolSubmitReview(reviewId, status, feedback) {
|
|
124
126
|
if (!state.registeredName) return { error: 'You must call register() first' };
|
|
125
127
|
|
|
@@ -131,6 +133,26 @@ module.exports = function (ctx) {
|
|
|
131
133
|
if (!review) return { error: `Review not found: ${reviewId}` };
|
|
132
134
|
if (review.requested_by === state.registeredName) return { error: 'Cannot review your own code.' };
|
|
133
135
|
|
|
136
|
+
// Enforce substantive feedback — rubber-stamping is not allowed
|
|
137
|
+
const feedbackText = (feedback || '').trim();
|
|
138
|
+
if (!feedbackText) {
|
|
139
|
+
return {
|
|
140
|
+
error: `Feedback is required. You must read "${review.file}" and describe what you found before submitting a review.`,
|
|
141
|
+
next_action: `Read the file "${review.file}" first, then call submit_review("${reviewId}", "${status}", "<your findings>").`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (feedbackText.length < REVIEW_FEEDBACK_MIN_LENGTH) {
|
|
145
|
+
return {
|
|
146
|
+
error: `Feedback too short (${feedbackText.length} chars, minimum ${REVIEW_FEEDBACK_MIN_LENGTH}). Describe specific findings — what you read, what issues you found or verified, and why you ${status === 'approved' ? 'approve' : 'request changes'}.`,
|
|
147
|
+
next_action: `Read the file "${review.file}" first, then call submit_review("${reviewId}", "${status}", "<your detailed findings>").`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Log audit entry for thin approvals (short feedback on an approval)
|
|
152
|
+
if (status === 'approved' && feedbackText.length < 150) {
|
|
153
|
+
logViolation('thin_review', state.registeredName, `Approved "${review.file}" with minimal feedback (${feedbackText.length} chars): "${feedbackText.substring(0, 100)}"`);
|
|
154
|
+
}
|
|
155
|
+
|
|
134
156
|
review.status = status;
|
|
135
157
|
review.reviewer = state.registeredName;
|
|
136
158
|
review.feedback = (feedback || '').substring(0, 2000);
|
|
@@ -191,7 +213,10 @@ module.exports = function (ctx) {
|
|
|
191
213
|
writeJsonFile(REVIEWS_FILE, reviews);
|
|
192
214
|
touchActivity();
|
|
193
215
|
|
|
194
|
-
const
|
|
216
|
+
const reviewNextAction = review.status === 'approved'
|
|
217
|
+
? 'Call listen() to continue.'
|
|
218
|
+
: 'Call listen() — the author will fix and resubmit.';
|
|
219
|
+
const result = { success: true, review_id: reviewId, status: review.status, next_action: reviewNextAction };
|
|
195
220
|
if (review.review_round) result.review_round = review.review_round;
|
|
196
221
|
if (review.auto_approved) result.auto_approved = true;
|
|
197
222
|
return result;
|
|
@@ -381,12 +406,12 @@ module.exports = function (ctx) {
|
|
|
381
406
|
{
|
|
382
407
|
name: 'request_review',
|
|
383
408
|
description: 'Request a code review from the team. Creates a review request and notifies all agents.',
|
|
384
|
-
inputSchema: { type: 'object', properties: { file_path: { type: 'string', description: 'File to review' }, description: { type: 'string', description: 'What to focus on in the review' } }, required: ['file_path'], additionalProperties: false },
|
|
409
|
+
inputSchema: { type: 'object', properties: { file_path: { type: 'string', description: 'File to review', maxLength: 500 }, description: { type: 'string', description: 'What to focus on in the review', maxLength: 2000 } }, required: ['file_path'], additionalProperties: false },
|
|
385
410
|
},
|
|
386
411
|
{
|
|
387
412
|
name: 'submit_review',
|
|
388
|
-
description: 'Submit a code review — approve or request changes
|
|
389
|
-
inputSchema: { type: 'object', properties: { review_id: { type: 'string', description: 'Review ID' }, status: { type: 'string', enum: ['approved', 'changes_requested'], description: 'Review result' }, feedback: { type: 'string', description: 'Your
|
|
413
|
+
description: 'Submit a code review — approve or request changes. You MUST read the file under review before calling this. Feedback is required (minimum 50 chars) and must describe specific findings — what you read, what issues you found or confirmed. Rubber-stamp approvals are rejected.',
|
|
414
|
+
inputSchema: { type: 'object', properties: { review_id: { type: 'string', description: 'Review ID', maxLength: 50 }, status: { type: 'string', enum: ['approved', 'changes_requested'], description: 'Review result' }, feedback: { type: 'string', description: 'Your findings from reading the file (required, min 50 chars). Describe what you read and what you found — bugs, security issues, correctness, or confirmation that the code is clean.', maxLength: 2000, minLength: 50 } }, required: ['review_id', 'status', 'feedback'], additionalProperties: false },
|
|
390
415
|
},
|
|
391
416
|
// Rules
|
|
392
417
|
{
|
|
@@ -395,7 +420,7 @@ module.exports = function (ctx) {
|
|
|
395
420
|
inputSchema: {
|
|
396
421
|
type: 'object',
|
|
397
422
|
properties: {
|
|
398
|
-
text: { type: 'string', description: 'The rule text' },
|
|
423
|
+
text: { type: 'string', description: 'The rule text', maxLength: 2000 },
|
|
399
424
|
category: { type: 'string', description: 'Rule category: safety, workflow, code-style, communication, custom' },
|
|
400
425
|
scope: {
|
|
401
426
|
type: 'object',
|
|
@@ -430,7 +455,7 @@ module.exports = function (ctx) {
|
|
|
430
455
|
{
|
|
431
456
|
name: 'log_violation',
|
|
432
457
|
description: 'Log a workflow rule violation to the audit trail. Used automatically by review gates, or manually to flag issues.',
|
|
433
|
-
inputSchema: { type: 'object', properties: { type: { type: 'string', description: 'Violation type: review_skipped, push_without_approval, rule_violated, etc.' }, details: { type: 'string', description: 'Description of the violation' } }, required: ['type'], additionalProperties: false },
|
|
458
|
+
inputSchema: { type: 'object', properties: { type: { type: 'string', description: 'Violation type: review_skipped, push_without_approval, rule_violated, etc.', maxLength: 100 }, details: { type: 'string', description: 'Description of the violation', maxLength: 2000 } }, required: ['type'], additionalProperties: false },
|
|
434
459
|
},
|
|
435
460
|
{
|
|
436
461
|
name: 'request_push_approval',
|
package/tools/knowledge.js
CHANGED
|
@@ -132,7 +132,7 @@ module.exports = function (ctx) {
|
|
|
132
132
|
total_messages: compressed.segments.reduce((s, seg) => s + seg.message_count, 0) + recent.length,
|
|
133
133
|
compressed_count: compressed.segments.reduce((s, seg) => s + seg.message_count, 0),
|
|
134
134
|
recent_count: recent.length,
|
|
135
|
-
|
|
135
|
+
next_action: 'Call listen() to receive messages.',
|
|
136
136
|
};
|
|
137
137
|
}
|
|
138
138
|
|
|
@@ -223,9 +223,9 @@ module.exports = function (ctx) {
|
|
|
223
223
|
progress,
|
|
224
224
|
your_tasks: myActiveTasks.map(t => ({ id: t.id, title: t.title, status: t.status })),
|
|
225
225
|
your_completed: myCompletedCount,
|
|
226
|
-
|
|
227
|
-
? `You have ${myActiveTasks.length} active task(s). Continue working.`
|
|
228
|
-
: '
|
|
226
|
+
next_action: myActiveTasks.length > 0
|
|
227
|
+
? `You have ${myActiveTasks.length} active task(s). Continue working, then call listen().`
|
|
228
|
+
: 'Call listen() to receive messages and start working.',
|
|
229
229
|
};
|
|
230
230
|
}
|
|
231
231
|
|
package/tools/messaging.js
CHANGED
|
@@ -56,7 +56,9 @@ module.exports = function (ctx) {
|
|
|
56
56
|
result.preview = `${latest.from}: "${latest.content.substring(0, 80).replace(/\n/g, ' ')}..."`;
|
|
57
57
|
const oldestAge = Math.round((Date.now() - new Date(unconsumed[0].timestamp).getTime()) / 1000);
|
|
58
58
|
result.urgency = oldestAge > 120 ? 'critical' : oldestAge > 30 ? 'urgent' : 'normal';
|
|
59
|
-
result.
|
|
59
|
+
result.next_action = 'Call listen() to receive and process these messages.';
|
|
60
|
+
} else {
|
|
61
|
+
result.next_action = 'Call listen() to wait for new messages.';
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
return result;
|
package/tools/safety.js
CHANGED
|
@@ -34,7 +34,7 @@ module.exports = function (ctx) {
|
|
|
34
34
|
locks[normalized] = { agent: state.registeredName, since: new Date().toISOString() };
|
|
35
35
|
writeJsonFile(LOCKS_FILE, locks);
|
|
36
36
|
touchActivity();
|
|
37
|
-
return { success: true, file: normalized,
|
|
37
|
+
return { success: true, file: normalized, next_action: 'Edit the file, then call unlock_file() when done.' };
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
function toolUnlockFile(filePath) {
|
|
@@ -48,15 +48,15 @@ module.exports = function (ctx) {
|
|
|
48
48
|
if (lock.agent === state.registeredName) { delete locks[fp]; count++; }
|
|
49
49
|
}
|
|
50
50
|
writeJsonFile(LOCKS_FILE, locks);
|
|
51
|
-
return { success: true, unlocked: count, message: `Unlocked ${count} file(s)
|
|
51
|
+
return { success: true, unlocked: count, message: `Unlocked ${count} file(s).`, next_action: 'Call listen() to receive messages.' };
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
if (!locks[normalized]) return { success: true, message: 'File was not locked.' };
|
|
54
|
+
if (!locks[normalized]) return { success: true, message: 'File was not locked.', next_action: 'Call listen() to receive messages.' };
|
|
55
55
|
if (locks[normalized].agent !== state.registeredName) return { error: `File is locked by ${locks[normalized].agent}, not you.` };
|
|
56
56
|
|
|
57
57
|
delete locks[normalized];
|
|
58
58
|
writeJsonFile(LOCKS_FILE, locks);
|
|
59
|
-
return { success: true, file: normalized, message: 'File unlocked.' };
|
|
59
|
+
return { success: true, file: normalized, message: 'File unlocked.', next_action: 'Call listen() to receive messages.' };
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// --- Dependencies ---
|
package/tools/tasks.js
CHANGED
|
@@ -81,7 +81,7 @@ module.exports = function (ctx) {
|
|
|
81
81
|
saveTasks(tasks);
|
|
82
82
|
touchActivity();
|
|
83
83
|
|
|
84
|
-
const result = { success: true, task_id: task.id, assignee: task.assignee };
|
|
84
|
+
const result = { success: true, task_id: task.id, assignee: task.assignee, next_action: 'Call listen() to receive updates.' };
|
|
85
85
|
if (taskChannel) result.channel = taskChannel;
|
|
86
86
|
return result;
|
|
87
87
|
}
|
|
@@ -160,6 +160,7 @@ module.exports = function (ctx) {
|
|
|
160
160
|
task_id: task.id,
|
|
161
161
|
status: 'in_review',
|
|
162
162
|
review_id: reviewId,
|
|
163
|
+
next_action: 'Call listen() to wait for the reviewer to approve.',
|
|
163
164
|
message: `Cannot mark done — a reviewer is online and no approval exists. Review ${reviewId} auto-created. Wait for approval, then try again.`,
|
|
164
165
|
};
|
|
165
166
|
}
|
|
@@ -291,7 +292,11 @@ module.exports = function (ctx) {
|
|
|
291
292
|
for (const n of notifications) { helpers.sendSystemMessage(n.agent, n.message); }
|
|
292
293
|
} catch (e) { /* hooks not available */ }
|
|
293
294
|
|
|
294
|
-
|
|
295
|
+
const nextAction = status === 'done' ? 'Send a summary of what you did via send_message(), then call listen().'
|
|
296
|
+
: status === 'in_progress' ? `Do the work on "${task.title}", then call update_task("${task.id}", "done") when finished.`
|
|
297
|
+
: status === 'blocked' ? 'Send a message explaining the blocker, then call listen().'
|
|
298
|
+
: 'Call listen() to receive updates.';
|
|
299
|
+
return { success: true, task_id: task.id, status: task.status, title: task.title, next_action: nextAction };
|
|
295
300
|
}
|
|
296
301
|
|
|
297
302
|
// --- List Tasks ---
|
|
@@ -393,9 +398,9 @@ module.exports = function (ctx) {
|
|
|
393
398
|
inputSchema: {
|
|
394
399
|
type: 'object',
|
|
395
400
|
properties: {
|
|
396
|
-
title: { type: 'string', description: 'Short task title' },
|
|
397
|
-
description: { type: 'string', description: 'Detailed task description' },
|
|
398
|
-
assignee: { type: 'string', description: 'Agent to assign to (optional, auto-assigns with 2 agents)' },
|
|
401
|
+
title: { type: 'string', description: 'Short task title', maxLength: 200 },
|
|
402
|
+
description: { type: 'string', description: 'Detailed task description', maxLength: 5000 },
|
|
403
|
+
assignee: { type: 'string', description: 'Agent to assign to (optional, auto-assigns with 2 agents)', maxLength: 50 },
|
|
399
404
|
},
|
|
400
405
|
required: ['title'],
|
|
401
406
|
additionalProperties: false,
|
|
@@ -407,9 +412,9 @@ module.exports = function (ctx) {
|
|
|
407
412
|
inputSchema: {
|
|
408
413
|
type: 'object',
|
|
409
414
|
properties: {
|
|
410
|
-
task_id: { type: 'string', description: 'Task ID to update' },
|
|
415
|
+
task_id: { type: 'string', description: 'Task ID to update', maxLength: 50 },
|
|
411
416
|
status: { type: 'string', enum: ['pending', 'in_progress', 'in_review', 'done', 'blocked', 'blocked_permanent'], description: 'New status' },
|
|
412
|
-
notes: { type: 'string', description: 'Optional progress note' },
|
|
417
|
+
notes: { type: 'string', description: 'Optional progress note', maxLength: 2000 },
|
|
413
418
|
},
|
|
414
419
|
required: ['task_id', 'status'],
|
|
415
420
|
additionalProperties: false,
|
package/tools/workflows.js
CHANGED
|
@@ -102,7 +102,7 @@ module.exports = function (ctx) {
|
|
|
102
102
|
autonomous,
|
|
103
103
|
parallel,
|
|
104
104
|
started_steps: startedSteps.map(s => ({ id: s.id, description: s.description, assignee: s.assignee })),
|
|
105
|
-
|
|
105
|
+
next_action: autonomous ? 'Call get_work() for your assignment.' : 'Call listen() to receive updates.',
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -179,6 +179,7 @@ module.exports = function (ctx) {
|
|
|
179
179
|
next_steps: nextSteps.length > 0 ? nextSteps.map(s => ({ id: s.id, description: s.description, assignee: s.assignee })) : null,
|
|
180
180
|
progress: `${doneCount}/${wf.steps.length} (${pct}%)`,
|
|
181
181
|
workflow_status: wf.status,
|
|
182
|
+
next_action: wf.autonomous ? 'Call get_work() for your next assignment.' : 'Call listen() to receive the next step.',
|
|
182
183
|
};
|
|
183
184
|
}
|
|
184
185
|
|