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.
- package/CHANGELOG.md +269 -77
- package/README.md +66 -63
- package/SECURITY.md +8 -6
- package/cli.js +377 -35
- package/conversation-templates/autonomous-feature.json +54 -4
- package/conversation-templates/code-review.json +41 -3
- package/conversation-templates/debug-squad.json +41 -3
- package/conversation-templates/feature-build.json +41 -3
- package/conversation-templates/research-write.json +41 -3
- package/dashboard.html +3954 -921
- package/dashboard.js +1192 -153
- package/design-system.css +708 -0
- package/design-system.html +264 -0
- package/lib/agents.js +20 -6
- package/lib/audit.js +417 -0
- package/lib/codex-neohive-toml.js +34 -0
- package/lib/compact.js +5 -2
- package/lib/config.js +4 -3
- package/lib/file-io.js +3 -3
- package/lib/github-sync.js +291 -0
- package/lib/hooks.js +173 -0
- package/lib/ide-activity.js +121 -0
- package/lib/resolve-server-data-dir.js +96 -0
- package/logo.svg +1 -0
- package/package.json +12 -3
- package/scripts/check-portable-paths.mjs +74 -0
- package/server.js +1986 -857
- package/templates/debate.json +24 -5
- package/templates/managed.json +48 -9
- package/templates/pair.json +22 -3
- package/templates/review.json +26 -5
- package/templates/team.json +38 -8
- package/tools/channels.js +116 -0
- package/tools/governance.js +471 -0
- package/tools/hooks.js +65 -0
- package/tools/knowledge.js +301 -0
- package/tools/messaging.js +321 -0
- package/tools/safety.js +144 -0
- package/tools/system.js +198 -0
- package/tools/tasks.js +446 -0
- package/tools/workflows.js +286 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Governance tools: voting, reviews, rules, push approval, audit logging.
|
|
4
|
+
// Extracted from server.js as proof of concept for modular tool architecture.
|
|
5
|
+
//
|
|
6
|
+
// Usage in server.js:
|
|
7
|
+
// const governance = require('./tools/governance')(ctx);
|
|
8
|
+
// // ctx = { state, helpers, files }
|
|
9
|
+
// // governance.handlers.call_vote(args) => result
|
|
10
|
+
// // governance.definitions => array of MCP tool schemas
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
|
|
14
|
+
module.exports = function (ctx) {
|
|
15
|
+
const { state, helpers, files } = ctx;
|
|
16
|
+
|
|
17
|
+
// Destructure helpers for concise access
|
|
18
|
+
const {
|
|
19
|
+
getVotes, getReviews, getRules, getPushRequests,
|
|
20
|
+
getAgents, isPidAlive, getReputation, getTasks, saveTasks,
|
|
21
|
+
generateId, readJsonFile, writeJsonFile, cachedRead, invalidateCache,
|
|
22
|
+
broadcastSystemMessage, sendSystemMessage, touchActivity, fireEvent,
|
|
23
|
+
} = helpers;
|
|
24
|
+
|
|
25
|
+
const {
|
|
26
|
+
VOTES_FILE, REVIEWS_FILE, RULES_FILE,
|
|
27
|
+
PUSH_REQUESTS_FILE, AUDIT_LOG_FILE, REPUTATION_FILE,
|
|
28
|
+
} = files;
|
|
29
|
+
|
|
30
|
+
// --- Voting ---
|
|
31
|
+
|
|
32
|
+
function toolCallVote(question, options) {
|
|
33
|
+
if (!state.registeredName) return { error: 'You must call register() first' };
|
|
34
|
+
if (typeof question !== 'string' || question.length < 1 || question.length > 200) return { error: 'Question must be 1-200 chars' };
|
|
35
|
+
if (!Array.isArray(options) || options.length < 2 || options.length > 10) return { error: 'Need 2-10 options' };
|
|
36
|
+
|
|
37
|
+
const votes = getVotes();
|
|
38
|
+
if (votes.length >= 500) return { error: 'Vote limit reached (max 500).' };
|
|
39
|
+
const vote = {
|
|
40
|
+
id: 'vote_' + generateId(),
|
|
41
|
+
question,
|
|
42
|
+
options: options.map(o => String(o).substring(0, 50)),
|
|
43
|
+
votes: {},
|
|
44
|
+
status: 'open',
|
|
45
|
+
created_by: state.registeredName,
|
|
46
|
+
created_at: new Date().toISOString(),
|
|
47
|
+
};
|
|
48
|
+
votes.push(vote);
|
|
49
|
+
writeJsonFile(VOTES_FILE, votes);
|
|
50
|
+
|
|
51
|
+
broadcastSystemMessage(`[VOTE] ${state.registeredName} started a vote: "${question}" — Options: ${vote.options.join(', ')}. Call cast_vote("${vote.id}", "your_choice") to vote.`, state.registeredName);
|
|
52
|
+
touchActivity();
|
|
53
|
+
return { success: true, vote_id: vote.id, question, options: vote.options, message: 'Vote created. All agents have been notified.' };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function toolCastVote(voteId, choice) {
|
|
57
|
+
if (!state.registeredName) return { error: 'You must call register() first' };
|
|
58
|
+
|
|
59
|
+
const votes = getVotes();
|
|
60
|
+
const vote = votes.find(v => v.id === voteId);
|
|
61
|
+
if (!vote) return { error: `Vote not found: ${voteId}` };
|
|
62
|
+
if (vote.status !== 'open') return { error: 'Vote is already closed.' };
|
|
63
|
+
if (!vote.options.includes(choice)) return { error: `Invalid choice. Options: ${vote.options.join(', ')}` };
|
|
64
|
+
|
|
65
|
+
vote.votes[state.registeredName] = { choice, voted_at: new Date().toISOString() };
|
|
66
|
+
|
|
67
|
+
const agents = getAgents();
|
|
68
|
+
const onlineAgents = Object.keys(agents).filter(n => isPidAlive(agents[n].pid, agents[n].last_activity));
|
|
69
|
+
const allVoted = onlineAgents.every(n => vote.votes[n]);
|
|
70
|
+
|
|
71
|
+
if (allVoted) {
|
|
72
|
+
vote.status = 'closed';
|
|
73
|
+
vote.closed_at = new Date().toISOString();
|
|
74
|
+
const results = {};
|
|
75
|
+
for (const opt of vote.options) results[opt] = 0;
|
|
76
|
+
for (const v of Object.values(vote.votes)) results[v.choice]++;
|
|
77
|
+
vote.results = results;
|
|
78
|
+
const winner = Object.entries(results).sort((a, b) => b[1] - a[1])[0];
|
|
79
|
+
broadcastSystemMessage(`[VOTE RESULT] "${vote.question}" — Winner: ${winner[0]} (${winner[1]} votes). Full results: ${JSON.stringify(results)}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
writeJsonFile(VOTES_FILE, votes);
|
|
83
|
+
touchActivity();
|
|
84
|
+
return { success: true, vote_id: voteId, your_vote: choice, status: vote.status, votes_cast: Object.keys(vote.votes).length, agents_online: onlineAgents.length };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function toolVoteStatus(voteId) {
|
|
88
|
+
const votes = getVotes();
|
|
89
|
+
if (voteId) {
|
|
90
|
+
const vote = votes.find(v => v.id === voteId);
|
|
91
|
+
if (!vote) return { error: `Vote not found: ${voteId}` };
|
|
92
|
+
return { vote };
|
|
93
|
+
}
|
|
94
|
+
return { votes: votes.map(v => ({ id: v.id, question: v.question, status: v.status, votes_cast: Object.keys(v.votes).length, results: v.results || null })) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --- Code Reviews ---
|
|
98
|
+
|
|
99
|
+
function toolRequestReview(filePath, description) {
|
|
100
|
+
if (!state.registeredName) return { error: 'You must call register() first' };
|
|
101
|
+
if (typeof filePath !== 'string' || filePath.length < 1) return { error: 'File path required' };
|
|
102
|
+
|
|
103
|
+
const reviews = getReviews();
|
|
104
|
+
if (reviews.length >= 500) return { error: 'Review limit reached (max 500).' };
|
|
105
|
+
const review = {
|
|
106
|
+
id: 'rev_' + generateId(),
|
|
107
|
+
file: filePath.replace(/\\/g, '/'),
|
|
108
|
+
description: (description || '').substring(0, 500),
|
|
109
|
+
status: 'pending',
|
|
110
|
+
requested_by: state.registeredName,
|
|
111
|
+
requested_at: new Date().toISOString(),
|
|
112
|
+
reviewer: null,
|
|
113
|
+
feedback: null,
|
|
114
|
+
};
|
|
115
|
+
reviews.push(review);
|
|
116
|
+
writeJsonFile(REVIEWS_FILE, reviews);
|
|
117
|
+
|
|
118
|
+
broadcastSystemMessage(`[REVIEW] ${state.registeredName} requests review of "${review.file}": ${review.description || 'No description'}. Call submit_review("${review.id}", "approved"/"changes_requested", "your feedback") to review.`, state.registeredName);
|
|
119
|
+
touchActivity();
|
|
120
|
+
return { success: true, review_id: review.id, file: review.file, message: 'Review requested. Team has been notified.' };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function toolSubmitReview(reviewId, status, feedback) {
|
|
124
|
+
if (!state.registeredName) return { error: 'You must call register() first' };
|
|
125
|
+
|
|
126
|
+
const validStatuses = ['approved', 'changes_requested'];
|
|
127
|
+
if (!validStatuses.includes(status)) return { error: `Status must be: ${validStatuses.join(' or ')}` };
|
|
128
|
+
|
|
129
|
+
const reviews = getReviews();
|
|
130
|
+
const review = reviews.find(r => r.id === reviewId);
|
|
131
|
+
if (!review) return { error: `Review not found: ${reviewId}` };
|
|
132
|
+
if (review.requested_by === state.registeredName) return { error: 'Cannot review your own code.' };
|
|
133
|
+
|
|
134
|
+
review.status = status;
|
|
135
|
+
review.reviewer = state.registeredName;
|
|
136
|
+
review.feedback = (feedback || '').substring(0, 2000);
|
|
137
|
+
review.reviewed_at = new Date().toISOString();
|
|
138
|
+
|
|
139
|
+
if (status === 'changes_requested') {
|
|
140
|
+
review.review_round = (review.review_round || 0) + 1;
|
|
141
|
+
|
|
142
|
+
// Circuit breaker: track consecutive rejections
|
|
143
|
+
const rep = getReputation();
|
|
144
|
+
if (!rep[review.requested_by]) rep[review.requested_by] = { tasks_completed: 0, reviews_done: 0, messages_sent: 0, consecutive_rejections: 0, first_seen: new Date().toISOString(), last_active: new Date().toISOString(), strengths: [], task_times: [], response_times: [] };
|
|
145
|
+
rep[review.requested_by].consecutive_rejections = (rep[review.requested_by].consecutive_rejections || 0) + 1;
|
|
146
|
+
if (rep[review.requested_by].consecutive_rejections >= 3) {
|
|
147
|
+
rep[review.requested_by].demoted = true;
|
|
148
|
+
rep[review.requested_by].demoted_at = new Date().toISOString();
|
|
149
|
+
sendSystemMessage(review.requested_by, `[CIRCUIT BREAKER] You have ${rep[review.requested_by].consecutive_rejections} consecutive rejections. You are being assigned simpler tasks until your next approval. Focus on smaller, well-tested changes.`);
|
|
150
|
+
}
|
|
151
|
+
writeJsonFile(REPUTATION_FILE, rep);
|
|
152
|
+
|
|
153
|
+
const tasks = getTasks();
|
|
154
|
+
const relatedTask = tasks.find(t => t.title && review.file && t.title.includes(review.file)) ||
|
|
155
|
+
tasks.find(t => t.assignee === review.requested_by && t.status === 'in_progress');
|
|
156
|
+
if (relatedTask) {
|
|
157
|
+
relatedTask.retry_expected = true;
|
|
158
|
+
relatedTask.review_feedback = review.feedback;
|
|
159
|
+
relatedTask.review_round = review.review_round;
|
|
160
|
+
if (review.review_round >= 2) {
|
|
161
|
+
relatedTask.auto_approve_next = true;
|
|
162
|
+
}
|
|
163
|
+
saveTasks(tasks);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const roundMsg = `[REVIEW FEEDBACK] ${state.registeredName} requested changes on "${review.file}": ${review.feedback}. Fix and re-submit. This is review round ${review.review_round}/2.` +
|
|
167
|
+
(review.review_round >= 2 ? ' FINAL ROUND — next submission will be auto-approved.' : '');
|
|
168
|
+
sendSystemMessage(review.requested_by, roundMsg);
|
|
169
|
+
} else {
|
|
170
|
+
const rep = getReputation();
|
|
171
|
+
if (rep[review.requested_by]) {
|
|
172
|
+
rep[review.requested_by].consecutive_rejections = 0;
|
|
173
|
+
rep[review.requested_by].demoted = false;
|
|
174
|
+
writeJsonFile(REPUTATION_FILE, rep);
|
|
175
|
+
}
|
|
176
|
+
const agents = getAgents();
|
|
177
|
+
if (agents[review.requested_by]) {
|
|
178
|
+
sendSystemMessage(review.requested_by, `[REVIEW] ${state.registeredName} approved "${review.file}": ${review.feedback || 'Looks good!'}`);
|
|
179
|
+
}
|
|
180
|
+
fireEvent('review_approved', { file: review.file, reviewer: state.registeredName, author: review.requested_by });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Auto-approve if exceeded max review rounds
|
|
184
|
+
if (status === 'changes_requested' && review.review_round > 2) {
|
|
185
|
+
review.status = 'approved';
|
|
186
|
+
review.auto_approved = true;
|
|
187
|
+
review.auto_approve_reason = `Auto-approved after ${review.review_round} review rounds (max 2 rounds exceeded).`;
|
|
188
|
+
sendSystemMessage(review.requested_by, `[REVIEW] "${review.file}" auto-approved after ${review.review_round} review rounds. Flagged for later human review.`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
writeJsonFile(REVIEWS_FILE, reviews);
|
|
192
|
+
touchActivity();
|
|
193
|
+
|
|
194
|
+
const result = { success: true, review_id: reviewId, status: review.status, message: `Review submitted: ${review.status}` };
|
|
195
|
+
if (review.review_round) result.review_round = review.review_round;
|
|
196
|
+
if (review.auto_approved) result.auto_approved = true;
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// --- Rules ---
|
|
201
|
+
|
|
202
|
+
function toolAddRule(text, category, scope) {
|
|
203
|
+
if (!state.registeredName) return { error: 'You must call register() first' };
|
|
204
|
+
if (!text || !text.trim()) return { error: 'Rule text cannot be empty' };
|
|
205
|
+
category = category || 'custom';
|
|
206
|
+
const validCategories = ['safety', 'workflow', 'code-style', 'communication', 'custom'];
|
|
207
|
+
if (!validCategories.includes(category)) return { error: `Category must be one of: ${validCategories.join(', ')}` };
|
|
208
|
+
if (scope && typeof scope !== 'object') return { error: 'scope must be an object with optional fields: role, provider, agent' };
|
|
209
|
+
|
|
210
|
+
const rules = getRules();
|
|
211
|
+
const rule = {
|
|
212
|
+
id: 'rule_' + generateId(),
|
|
213
|
+
text: text.trim(),
|
|
214
|
+
category,
|
|
215
|
+
created_by: state.registeredName,
|
|
216
|
+
created_at: new Date().toISOString(),
|
|
217
|
+
active: true,
|
|
218
|
+
};
|
|
219
|
+
if (scope) {
|
|
220
|
+
if (scope.role) rule.scope_role = String(scope.role).toLowerCase();
|
|
221
|
+
if (scope.provider) rule.scope_provider = String(scope.provider).toLowerCase();
|
|
222
|
+
if (scope.agent) rule.scope_agent = String(scope.agent);
|
|
223
|
+
}
|
|
224
|
+
rules.push(rule);
|
|
225
|
+
writeJsonFile(RULES_FILE, rules);
|
|
226
|
+
const scopeMsg = scope ? ` (scoped to ${JSON.stringify(scope)})` : '';
|
|
227
|
+
return { success: true, rule_id: rule.id, message: `Rule added: "${text.substring(0, 80)}"${scopeMsg}. Matching agents will see this in their guide.` };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function toolListRules() {
|
|
231
|
+
const rules = getRules();
|
|
232
|
+
const active = rules.filter(r => r.active);
|
|
233
|
+
const inactive = rules.filter(r => !r.active);
|
|
234
|
+
return {
|
|
235
|
+
rules: active,
|
|
236
|
+
inactive_count: inactive.length,
|
|
237
|
+
total: rules.length,
|
|
238
|
+
categories: [...new Set(active.map(r => r.category))],
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function toolRemoveRule(ruleId) {
|
|
243
|
+
if (!state.registeredName) return { error: 'You must call register() first' };
|
|
244
|
+
if (!ruleId) return { error: 'rule_id is required' };
|
|
245
|
+
const rules = getRules();
|
|
246
|
+
const idx = rules.findIndex(r => r.id === ruleId);
|
|
247
|
+
if (idx === -1) return { error: `Rule not found: ${ruleId}` };
|
|
248
|
+
const removed = rules.splice(idx, 1)[0];
|
|
249
|
+
writeJsonFile(RULES_FILE, rules);
|
|
250
|
+
return { success: true, removed: removed.text.substring(0, 80), message: 'Rule removed.' };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function toolToggleRule(ruleId) {
|
|
254
|
+
if (!state.registeredName) return { error: 'You must call register() first' };
|
|
255
|
+
if (!ruleId) return { error: 'rule_id is required' };
|
|
256
|
+
const rules = getRules();
|
|
257
|
+
const rule = rules.find(r => r.id === ruleId);
|
|
258
|
+
if (!rule) return { error: `Rule not found: ${ruleId}` };
|
|
259
|
+
rule.active = !rule.active;
|
|
260
|
+
writeJsonFile(RULES_FILE, rules);
|
|
261
|
+
return { success: true, rule_id: ruleId, active: rule.active, message: `Rule ${rule.active ? 'activated' : 'deactivated'}.` };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// --- Audit log ---
|
|
265
|
+
|
|
266
|
+
function logViolation(type, agent, details) {
|
|
267
|
+
const entry = {
|
|
268
|
+
timestamp: new Date().toISOString(),
|
|
269
|
+
type,
|
|
270
|
+
agent,
|
|
271
|
+
details: (details || '').substring(0, 1000),
|
|
272
|
+
};
|
|
273
|
+
try {
|
|
274
|
+
fs.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + '\n');
|
|
275
|
+
} catch (e) { /* audit log write failed */ }
|
|
276
|
+
return entry;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function toolLogViolation(type, details) {
|
|
280
|
+
if (!state.registeredName) return { error: 'You must call register() first' };
|
|
281
|
+
if (!type) return { error: 'type is required (e.g., "review_skipped", "push_without_approval", "rule_violated")' };
|
|
282
|
+
const entry = logViolation(type, state.registeredName, details);
|
|
283
|
+
return { success: true, logged: entry, message: `Violation logged: ${type}` };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// --- Push approval ---
|
|
287
|
+
|
|
288
|
+
const PUSH_AUTO_APPROVE_MS = 120000; // 2 minutes
|
|
289
|
+
|
|
290
|
+
function toolRequestPushApproval(branch, description) {
|
|
291
|
+
if (!state.registeredName) return { error: 'You must call register() first' };
|
|
292
|
+
if (!branch) return { error: 'branch is required' };
|
|
293
|
+
|
|
294
|
+
const agents = getAgents();
|
|
295
|
+
const aliveOthers = Object.keys(agents).filter(n => n !== state.registeredName && isPidAlive(agents[n].pid, agents[n].last_activity));
|
|
296
|
+
|
|
297
|
+
if (aliveOthers.length === 0) {
|
|
298
|
+
return { approved: true, auto: true, message: 'No other agents online — auto-approved. You may push.' };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const requests = getPushRequests();
|
|
302
|
+
const id = 'push_' + generateId();
|
|
303
|
+
const request = {
|
|
304
|
+
id,
|
|
305
|
+
branch: branch.substring(0, 100),
|
|
306
|
+
description: (description || '').substring(0, 500),
|
|
307
|
+
requested_by: state.registeredName,
|
|
308
|
+
requested_at: new Date().toISOString(),
|
|
309
|
+
status: 'pending',
|
|
310
|
+
acked_by: null,
|
|
311
|
+
};
|
|
312
|
+
requests.push(request);
|
|
313
|
+
writeJsonFile(PUSH_REQUESTS_FILE, requests);
|
|
314
|
+
|
|
315
|
+
broadcastSystemMessage(`[PUSH REQUEST] ${state.registeredName} wants to push branch "${branch}". ${description || ''}. Call ack_push("${id}") to approve.`, state.registeredName);
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
request_id: id,
|
|
319
|
+
status: 'pending',
|
|
320
|
+
waiting_on: aliveOthers,
|
|
321
|
+
auto_approve_after: '2 minutes',
|
|
322
|
+
message: `Push request created. Waiting for approval from ${aliveOthers.join(', ')}. Auto-approves in 2 minutes if no response.`,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function toolAckPush(requestId) {
|
|
327
|
+
if (!state.registeredName) return { error: 'You must call register() first' };
|
|
328
|
+
if (!requestId) return { error: 'request_id is required' };
|
|
329
|
+
|
|
330
|
+
const requests = getPushRequests();
|
|
331
|
+
const req = requests.find(r => r.id === requestId);
|
|
332
|
+
if (!req) return { error: `Push request not found: ${requestId}` };
|
|
333
|
+
if (req.requested_by === state.registeredName) return { error: 'Cannot approve your own push request.' };
|
|
334
|
+
if (req.status !== 'pending') return { error: `Push request already ${req.status}.` };
|
|
335
|
+
|
|
336
|
+
req.status = 'approved';
|
|
337
|
+
req.acked_by = state.registeredName;
|
|
338
|
+
req.acked_at = new Date().toISOString();
|
|
339
|
+
writeJsonFile(PUSH_REQUESTS_FILE, requests);
|
|
340
|
+
|
|
341
|
+
sendSystemMessage(req.requested_by, `[PUSH APPROVED] ${state.registeredName} approved your push of "${req.branch}". You may push now.`);
|
|
342
|
+
|
|
343
|
+
return { success: true, request_id: requestId, message: `Push approved for ${req.requested_by} on branch "${req.branch}".` };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function checkPushAutoApprove(requestId) {
|
|
347
|
+
const requests = getPushRequests();
|
|
348
|
+
const req = requests.find(r => r.id === requestId);
|
|
349
|
+
if (!req || req.status !== 'pending') return;
|
|
350
|
+
|
|
351
|
+
const elapsed = Date.now() - new Date(req.requested_at).getTime();
|
|
352
|
+
if (elapsed >= PUSH_AUTO_APPROVE_MS) {
|
|
353
|
+
req.status = 'auto_approved';
|
|
354
|
+
req.acked_by = '__system__';
|
|
355
|
+
req.acked_at = new Date().toISOString();
|
|
356
|
+
writeJsonFile(PUSH_REQUESTS_FILE, requests);
|
|
357
|
+
sendSystemMessage(req.requested_by, `[PUSH AUTO-APPROVED] No response after 2 minutes. Push of "${req.branch}" auto-approved. You may push now.`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// --- MCP tool definitions ---
|
|
362
|
+
|
|
363
|
+
const definitions = [
|
|
364
|
+
// Voting
|
|
365
|
+
{
|
|
366
|
+
name: 'call_vote',
|
|
367
|
+
description: 'Start a vote for the team to decide something. All online agents are notified and can cast their vote.',
|
|
368
|
+
inputSchema: { type: 'object', properties: { question: { type: 'string', description: 'The question to vote on' }, options: { type: 'array', items: { type: 'string' }, description: 'Array of 2-10 options to choose from' } }, required: ['question', 'options'], additionalProperties: false },
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
name: 'cast_vote',
|
|
372
|
+
description: 'Cast your vote on an open vote. Vote auto-resolves when all online agents have voted.',
|
|
373
|
+
inputSchema: { type: 'object', properties: { vote_id: { type: 'string', description: 'Vote ID' }, choice: { type: 'string', description: 'Your choice (must match one of the options)' } }, required: ['vote_id', 'choice'], additionalProperties: false },
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
name: 'vote_status',
|
|
377
|
+
description: 'Check status of a specific vote or all votes.',
|
|
378
|
+
inputSchema: { type: 'object', properties: { vote_id: { type: 'string', description: 'Vote ID (optional — omit for all)' } }, additionalProperties: false },
|
|
379
|
+
},
|
|
380
|
+
// Reviews
|
|
381
|
+
{
|
|
382
|
+
name: 'request_review',
|
|
383
|
+
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 },
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
name: 'submit_review',
|
|
388
|
+
description: 'Submit a code review — approve or request changes with feedback.',
|
|
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 review feedback (max 2000 chars)' } }, required: ['review_id', 'status'], additionalProperties: false },
|
|
390
|
+
},
|
|
391
|
+
// Rules
|
|
392
|
+
{
|
|
393
|
+
name: 'add_rule',
|
|
394
|
+
description: 'Add a project rule. Rules appear in matching agents\' guide and briefing. Use scope to limit who sees the rule (omit for all agents). Categories: safety, workflow, code-style, communication, custom.',
|
|
395
|
+
inputSchema: {
|
|
396
|
+
type: 'object',
|
|
397
|
+
properties: {
|
|
398
|
+
text: { type: 'string', description: 'The rule text' },
|
|
399
|
+
category: { type: 'string', description: 'Rule category: safety, workflow, code-style, communication, custom' },
|
|
400
|
+
scope: {
|
|
401
|
+
type: 'object',
|
|
402
|
+
description: 'Optional scope filter. Omit for all agents.',
|
|
403
|
+
properties: {
|
|
404
|
+
role: { type: 'string', description: 'Only agents with this role (e.g., "quality", "backend")' },
|
|
405
|
+
provider: { type: 'string', description: 'Only agents on this platform (e.g., "claude", "cursor", "gemini")' },
|
|
406
|
+
agent: { type: 'string', description: 'Only this specific agent name' },
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
required: ['text'],
|
|
411
|
+
additionalProperties: false,
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
name: 'list_rules',
|
|
416
|
+
description: 'List all project rules (active and inactive count).',
|
|
417
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
name: 'remove_rule',
|
|
421
|
+
description: 'Remove a project rule by ID.',
|
|
422
|
+
inputSchema: { type: 'object', properties: { rule_id: { type: 'string', description: 'The rule ID to remove' } }, required: ['rule_id'], additionalProperties: false },
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
name: 'toggle_rule',
|
|
426
|
+
description: 'Toggle a rule active/inactive without deleting it.',
|
|
427
|
+
inputSchema: { type: 'object', properties: { rule_id: { type: 'string', description: 'The rule ID to toggle' } }, required: ['rule_id'], additionalProperties: false },
|
|
428
|
+
},
|
|
429
|
+
// Audit + Push
|
|
430
|
+
{
|
|
431
|
+
name: 'log_violation',
|
|
432
|
+
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 },
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
name: 'request_push_approval',
|
|
437
|
+
description: 'Request approval from another agent before pushing to a branch. Auto-approves after 2 minutes if no response, or immediately if no other agents are online.',
|
|
438
|
+
inputSchema: { type: 'object', properties: { branch: { type: 'string', description: 'Branch name to push (e.g., "main", "feature/xyz")' }, description: { type: 'string', description: 'What changes are being pushed' } }, required: ['branch'], additionalProperties: false },
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
name: 'ack_push',
|
|
442
|
+
description: 'Approve another agent\'s push request. Cannot approve your own.',
|
|
443
|
+
inputSchema: { type: 'object', properties: { request_id: { type: 'string', description: 'Push request ID from the system message' } }, required: ['request_id'], additionalProperties: false },
|
|
444
|
+
},
|
|
445
|
+
];
|
|
446
|
+
|
|
447
|
+
// Handler dispatch map: tool name -> function
|
|
448
|
+
const handlers = {
|
|
449
|
+
call_vote: function (args) { return toolCallVote(args.question, args.options); },
|
|
450
|
+
cast_vote: function (args) { return toolCastVote(args.vote_id, args.choice); },
|
|
451
|
+
vote_status: function (args) { return toolVoteStatus(args.vote_id); },
|
|
452
|
+
request_review: function (args) { return toolRequestReview(args.file_path, args.description); },
|
|
453
|
+
submit_review: function (args) { return toolSubmitReview(args.review_id, args.status, args.feedback); },
|
|
454
|
+
add_rule: function (args) { return toolAddRule(args.text, args.category, args.scope); },
|
|
455
|
+
list_rules: function () { return toolListRules(); },
|
|
456
|
+
remove_rule: function (args) { return toolRemoveRule(args.rule_id); },
|
|
457
|
+
toggle_rule: function (args) { return toolToggleRule(args.rule_id); },
|
|
458
|
+
log_violation: function (args) { return toolLogViolation(args.type, args.details); },
|
|
459
|
+
request_push_approval: function (args) { return toolRequestPushApproval(args.branch, args.description); },
|
|
460
|
+
ack_push: function (args) { return toolAckPush(args.request_id); },
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
definitions,
|
|
465
|
+
handlers,
|
|
466
|
+
// Expose internal helpers that server.js still needs (e.g., checkPushAutoApprove in heartbeat)
|
|
467
|
+
logViolation,
|
|
468
|
+
checkPushAutoApprove,
|
|
469
|
+
PUSH_AUTO_APPROVE_MS,
|
|
470
|
+
};
|
|
471
|
+
};
|
package/tools/hooks.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Hook tools: subscribe, unsubscribe, list hooks.
|
|
4
|
+
// Extracted as a tool module following the context injection pattern.
|
|
5
|
+
|
|
6
|
+
const hooksLib = require('../lib/hooks');
|
|
7
|
+
|
|
8
|
+
module.exports = function (ctx) {
|
|
9
|
+
const { state } = ctx;
|
|
10
|
+
|
|
11
|
+
function toolSubscribeHook(event, filter) {
|
|
12
|
+
if (!state.registeredName) return { error: 'You must call register() first' };
|
|
13
|
+
return hooksLib.subscribe(state.registeredName, event, filter || null);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function toolUnsubscribeHook(hookId) {
|
|
17
|
+
if (!state.registeredName) return { error: 'You must call register() first' };
|
|
18
|
+
return hooksLib.unsubscribe(state.registeredName, hookId || null);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function toolListHooks() {
|
|
22
|
+
if (!state.registeredName) return { error: 'You must call register() first' };
|
|
23
|
+
return hooksLib.listHooks(state.registeredName);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const definitions = [
|
|
27
|
+
{
|
|
28
|
+
name: 'subscribe_hook',
|
|
29
|
+
description: 'Subscribe to an event — you will receive automatic system messages when the event fires. Events: task.status_changed, agent.idle, agent.stuck, workflow.advanced, review.submitted. Use filter to narrow (e.g. { status: "done" } for only completed tasks).',
|
|
30
|
+
inputSchema: {
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
event: { type: 'string', enum: hooksLib.VALID_EVENTS, description: 'Event to subscribe to' },
|
|
34
|
+
filter: { type: 'object', description: 'Optional filter object (e.g. { status: "done", assignee: "Nick" })' },
|
|
35
|
+
},
|
|
36
|
+
required: ['event'],
|
|
37
|
+
additionalProperties: false,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'unsubscribe_hook',
|
|
42
|
+
description: 'Unsubscribe from a hook by ID, or omit hook_id to remove all your subscriptions.',
|
|
43
|
+
inputSchema: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
hook_id: { type: 'string', description: 'Hook ID to unsubscribe (optional — omit to remove all)' },
|
|
47
|
+
},
|
|
48
|
+
additionalProperties: false,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'list_hooks',
|
|
53
|
+
description: 'List your active event hook subscriptions.',
|
|
54
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const handlers = {
|
|
59
|
+
subscribe_hook: function (args) { return toolSubscribeHook(args.event, args.filter); },
|
|
60
|
+
unsubscribe_hook: function (args) { return toolUnsubscribeHook(args.hook_id); },
|
|
61
|
+
list_hooks: function () { return toolListHooks(); },
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return { definitions, handlers };
|
|
65
|
+
};
|