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
package/tools/safety.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Safety tools: file locking, dependencies.
|
|
4
|
+
// Extracted from server.js as part of modular tool architecture.
|
|
5
|
+
|
|
6
|
+
module.exports = function (ctx) {
|
|
7
|
+
const { state, helpers, files } = ctx;
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
getLocks, getAgents, isPidAlive, getTasks, getDeps,
|
|
11
|
+
generateId, writeJsonFile, touchActivity,
|
|
12
|
+
} = helpers;
|
|
13
|
+
|
|
14
|
+
const { LOCKS_FILE, DEPS_FILE } = files;
|
|
15
|
+
|
|
16
|
+
// --- File Locking ---
|
|
17
|
+
|
|
18
|
+
function toolLockFile(filePath) {
|
|
19
|
+
if (!state.registeredName) return { error: 'You must call register() first' };
|
|
20
|
+
if (typeof filePath !== 'string' || filePath.length < 1 || filePath.length > 200) return { error: 'Invalid file path' };
|
|
21
|
+
|
|
22
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
23
|
+
const locks = getLocks();
|
|
24
|
+
|
|
25
|
+
if (locks[normalized]) {
|
|
26
|
+
const holder = locks[normalized].agent;
|
|
27
|
+
if (holder === state.registeredName) return { success: true, message: 'You already hold this lock.', file: normalized };
|
|
28
|
+
const agents = getAgents();
|
|
29
|
+
if (agents[holder] && isPidAlive(agents[holder].pid, agents[holder].last_activity)) {
|
|
30
|
+
return { error: `File "${normalized}" is locked by ${holder} since ${locks[normalized].since}. Wait for them to unlock it or message them.` };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
locks[normalized] = { agent: state.registeredName, since: new Date().toISOString() };
|
|
35
|
+
writeJsonFile(LOCKS_FILE, locks);
|
|
36
|
+
touchActivity();
|
|
37
|
+
return { success: true, file: normalized, message: `File locked. Other agents cannot edit "${normalized}" until you call unlock_file().` };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function toolUnlockFile(filePath) {
|
|
41
|
+
if (!state.registeredName) return { error: 'You must call register() first' };
|
|
42
|
+
const normalized = (filePath || '').replace(/\\/g, '/');
|
|
43
|
+
const locks = getLocks();
|
|
44
|
+
|
|
45
|
+
if (!filePath) {
|
|
46
|
+
let count = 0;
|
|
47
|
+
for (const [fp, lock] of Object.entries(locks)) {
|
|
48
|
+
if (lock.agent === state.registeredName) { delete locks[fp]; count++; }
|
|
49
|
+
}
|
|
50
|
+
writeJsonFile(LOCKS_FILE, locks);
|
|
51
|
+
return { success: true, unlocked: count, message: `Unlocked ${count} file(s).` };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!locks[normalized]) return { success: true, message: 'File was not locked.' };
|
|
55
|
+
if (locks[normalized].agent !== state.registeredName) return { error: `File is locked by ${locks[normalized].agent}, not you.` };
|
|
56
|
+
|
|
57
|
+
delete locks[normalized];
|
|
58
|
+
writeJsonFile(LOCKS_FILE, locks);
|
|
59
|
+
return { success: true, file: normalized, message: 'File unlocked.' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// --- Dependencies ---
|
|
63
|
+
|
|
64
|
+
function toolDeclareDependency(taskId, dependsOnTaskId) {
|
|
65
|
+
if (!state.registeredName) return { error: 'You must call register() first' };
|
|
66
|
+
|
|
67
|
+
const tasks = getTasks();
|
|
68
|
+
const task = tasks.find(t => t.id === taskId);
|
|
69
|
+
const depTask = tasks.find(t => t.id === dependsOnTaskId);
|
|
70
|
+
if (!task) return { error: `Task not found: ${taskId}` };
|
|
71
|
+
if (!depTask) return { error: `Dependency task not found: ${dependsOnTaskId}` };
|
|
72
|
+
|
|
73
|
+
const deps = getDeps();
|
|
74
|
+
if (deps.length >= 1000) return { error: 'Dependency limit reached (max 1000).' };
|
|
75
|
+
deps.push({
|
|
76
|
+
id: 'dep_' + generateId(),
|
|
77
|
+
task_id: taskId,
|
|
78
|
+
depends_on: dependsOnTaskId,
|
|
79
|
+
declared_by: state.registeredName,
|
|
80
|
+
declared_at: new Date().toISOString(),
|
|
81
|
+
resolved: depTask.status === 'done',
|
|
82
|
+
});
|
|
83
|
+
writeJsonFile(DEPS_FILE, deps);
|
|
84
|
+
touchActivity();
|
|
85
|
+
|
|
86
|
+
if (depTask.status === 'done') {
|
|
87
|
+
return { success: true, message: `Dependency declared but already resolved — "${depTask.title}" is done. You can proceed.` };
|
|
88
|
+
}
|
|
89
|
+
return { success: true, message: `Dependency declared: "${task.title}" is blocked until "${depTask.title}" is done. You'll be notified when it completes.` };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function toolCheckDependencies(taskId) {
|
|
93
|
+
const deps = getDeps();
|
|
94
|
+
const tasks = getTasks();
|
|
95
|
+
|
|
96
|
+
if (taskId) {
|
|
97
|
+
const taskDeps = deps.filter(d => d.task_id === taskId);
|
|
98
|
+
return {
|
|
99
|
+
task_id: taskId,
|
|
100
|
+
dependencies: taskDeps.map(d => {
|
|
101
|
+
const t = tasks.find(t2 => t2.id === d.depends_on);
|
|
102
|
+
return { depends_on: d.depends_on, title: t ? t.title : 'unknown', status: t ? t.status : 'unknown', resolved: t ? t.status === 'done' : false };
|
|
103
|
+
}),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const unresolved = deps.filter(d => {
|
|
107
|
+
const t = tasks.find(t2 => t2.id === d.depends_on);
|
|
108
|
+
return t && t.status !== 'done';
|
|
109
|
+
});
|
|
110
|
+
return { unresolved_count: unresolved.length, unresolved: unresolved.map(d => ({ task_id: d.task_id, blocked_by: d.depends_on })) };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const definitions = [
|
|
114
|
+
{
|
|
115
|
+
name: 'lock_file',
|
|
116
|
+
description: 'Lock a file for exclusive editing. Other agents will be warned if they try to edit it. Call unlock_file() when done. Locks auto-release if you disconnect.',
|
|
117
|
+
inputSchema: { type: 'object', properties: { file_path: { type: 'string', description: 'Relative path to the file to lock' } }, required: ['file_path'], additionalProperties: false },
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'unlock_file',
|
|
121
|
+
description: 'Unlock a file you previously locked. Omit file_path to unlock all your files.',
|
|
122
|
+
inputSchema: { type: 'object', properties: { file_path: { type: 'string', description: 'File to unlock (optional — omit to unlock all)' } }, additionalProperties: false },
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: 'declare_dependency',
|
|
126
|
+
description: 'Declare that a task depends on another task. You will be notified when the dependency is complete.',
|
|
127
|
+
inputSchema: { type: 'object', properties: { task_id: { type: 'string', description: 'Your task that is blocked' }, depends_on: { type: 'string', description: 'Task ID that must complete first' } }, required: ['task_id', 'depends_on'], additionalProperties: false },
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'check_dependencies',
|
|
131
|
+
description: 'Check dependency status for a task or all unresolved dependencies.',
|
|
132
|
+
inputSchema: { type: 'object', properties: { task_id: { type: 'string', description: 'Task ID to check (optional — omit for all unresolved)' } }, additionalProperties: false },
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
const handlers = {
|
|
137
|
+
lock_file: function (args) { return toolLockFile(args.file_path); },
|
|
138
|
+
unlock_file: function (args) { return toolUnlockFile(args.file_path); },
|
|
139
|
+
declare_dependency: function (args) { return toolDeclareDependency(args.task_id, args.depends_on); },
|
|
140
|
+
check_dependencies: function (args) { return toolCheckDependencies(args.task_id); },
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return { definitions, handlers };
|
|
144
|
+
};
|
package/tools/system.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// System tools: profiles, workspaces, branches, reputation.
|
|
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 } = ctx;
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
getProfiles, saveProfiles, getWorkspace, saveWorkspace, ensureDataDir,
|
|
13
|
+
getAgents, getBranches, getHistoryFile, getReputation, touchActivity,
|
|
14
|
+
} = helpers;
|
|
15
|
+
|
|
16
|
+
// --- Profile ---
|
|
17
|
+
|
|
18
|
+
function toolUpdateProfile(displayName, avatar, bio, role) {
|
|
19
|
+
if (!state.registeredName) return { error: 'You must call register() first' };
|
|
20
|
+
|
|
21
|
+
const profiles = getProfiles();
|
|
22
|
+
if (!profiles[state.registeredName]) {
|
|
23
|
+
profiles[state.registeredName] = { display_name: state.registeredName, avatar: '', bio: '', role: '', created_at: new Date().toISOString() };
|
|
24
|
+
}
|
|
25
|
+
const p = profiles[state.registeredName];
|
|
26
|
+
if (displayName !== undefined && displayName !== null) {
|
|
27
|
+
if (typeof displayName !== 'string' || displayName.length > 30) return { error: 'display_name must be <= 30 chars' };
|
|
28
|
+
p.display_name = displayName;
|
|
29
|
+
}
|
|
30
|
+
if (avatar !== undefined && avatar !== null) {
|
|
31
|
+
if (typeof avatar !== 'string' || avatar.length > 65536) return { error: 'avatar too large (max 64KB)' };
|
|
32
|
+
p.avatar = avatar;
|
|
33
|
+
}
|
|
34
|
+
if (bio !== undefined && bio !== null) {
|
|
35
|
+
if (typeof bio !== 'string' || bio.length > 200) return { error: 'bio must be <= 200 chars' };
|
|
36
|
+
p.bio = bio;
|
|
37
|
+
}
|
|
38
|
+
if (role !== undefined && role !== null) {
|
|
39
|
+
if (typeof role !== 'string' || role.length > 30) return { error: 'role must be <= 30 chars' };
|
|
40
|
+
p.role = role;
|
|
41
|
+
}
|
|
42
|
+
p.updated_at = new Date().toISOString();
|
|
43
|
+
saveProfiles(profiles);
|
|
44
|
+
return { success: true, profile: p };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// --- Workspace ---
|
|
48
|
+
|
|
49
|
+
function toolWorkspaceWrite(key, content) {
|
|
50
|
+
if (!state.registeredName) return { error: 'You must call register() first' };
|
|
51
|
+
if (typeof key !== 'string' || key.length < 1 || key.length > 50) return { error: 'key must be 1-50 chars' };
|
|
52
|
+
if (!/^[a-zA-Z0-9_\-\.]+$/.test(key)) return { error: 'key must be alphanumeric/underscore/hyphen/dot' };
|
|
53
|
+
if (typeof content !== 'string') return { error: 'content must be a string' };
|
|
54
|
+
if (Buffer.byteLength(content, 'utf8') > 102400) return { error: 'content exceeds 100KB limit' };
|
|
55
|
+
|
|
56
|
+
ensureDataDir();
|
|
57
|
+
const ws = getWorkspace(state.registeredName);
|
|
58
|
+
if (!ws[key] && Object.keys(ws).length >= 50) return { error: 'Maximum 50 keys per workspace' };
|
|
59
|
+
ws[key] = { content, updated_at: new Date().toISOString() };
|
|
60
|
+
saveWorkspace(state.registeredName, ws);
|
|
61
|
+
touchActivity();
|
|
62
|
+
return { success: true, key, size: content.length, total_keys: Object.keys(ws).length };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function toolWorkspaceRead(key, agent) {
|
|
66
|
+
if (!state.registeredName) return { error: 'You must call register() first' };
|
|
67
|
+
const targetAgent = agent || state.registeredName;
|
|
68
|
+
if (targetAgent !== state.registeredName && !/^[a-zA-Z0-9_-]{1,20}$/.test(targetAgent)) {
|
|
69
|
+
return { error: 'Invalid agent name' };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const ws = getWorkspace(targetAgent);
|
|
73
|
+
if (key) {
|
|
74
|
+
if (!ws[key]) return { error: `Key "${key}" not found in ${targetAgent}'s workspace` };
|
|
75
|
+
return { agent: targetAgent, key, content: ws[key].content, updated_at: ws[key].updated_at };
|
|
76
|
+
}
|
|
77
|
+
const entries = {};
|
|
78
|
+
for (const [k, v] of Object.entries(ws)) {
|
|
79
|
+
entries[k] = { content: v.content, updated_at: v.updated_at };
|
|
80
|
+
}
|
|
81
|
+
return { agent: targetAgent, entries, total_keys: Object.keys(ws).length };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function toolWorkspaceList(agent) {
|
|
85
|
+
const agents = getAgents();
|
|
86
|
+
if (agent) {
|
|
87
|
+
if (!/^[a-zA-Z0-9_-]{1,20}$/.test(agent)) return { error: 'Invalid agent name' };
|
|
88
|
+
const ws = getWorkspace(agent);
|
|
89
|
+
return { agent, keys: Object.keys(ws).map(k => ({ key: k, size: ws[k].content.length, updated_at: ws[k].updated_at })) };
|
|
90
|
+
}
|
|
91
|
+
const result = {};
|
|
92
|
+
for (const name of Object.keys(agents)) {
|
|
93
|
+
const ws = getWorkspace(name);
|
|
94
|
+
result[name] = { key_count: Object.keys(ws).length, keys: Object.keys(ws) };
|
|
95
|
+
}
|
|
96
|
+
return { workspaces: result };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// --- Branches ---
|
|
100
|
+
|
|
101
|
+
function toolListBranches() {
|
|
102
|
+
const branches = getBranches();
|
|
103
|
+
const result = {};
|
|
104
|
+
for (const [name, info] of Object.entries(branches)) {
|
|
105
|
+
const histFile = getHistoryFile(name);
|
|
106
|
+
let msgCount = 0;
|
|
107
|
+
if (fs.existsSync(histFile)) {
|
|
108
|
+
const content = fs.readFileSync(histFile, 'utf8').trim();
|
|
109
|
+
if (content) msgCount = content.split(/\r?\n/).filter(l => l.trim()).length;
|
|
110
|
+
}
|
|
111
|
+
result[name] = { ...info, message_count: msgCount, is_current: name === state.currentBranch };
|
|
112
|
+
}
|
|
113
|
+
return { branches: result, current: state.currentBranch };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// --- Reputation ---
|
|
117
|
+
|
|
118
|
+
function toolGetReputation(agent) {
|
|
119
|
+
const rep = getReputation();
|
|
120
|
+
|
|
121
|
+
if (agent) {
|
|
122
|
+
if (!rep[agent]) return { agent, message: 'No reputation data yet for this agent.' };
|
|
123
|
+
return { agent, reputation: rep[agent] };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const leaderboard = Object.entries(rep).map(([name, r]) => {
|
|
127
|
+
const avgTaskTime = r.task_times && r.task_times.length > 0
|
|
128
|
+
? Math.round(r.task_times.reduce((a, b) => a + b, 0) / r.task_times.length) : null;
|
|
129
|
+
return {
|
|
130
|
+
agent: name,
|
|
131
|
+
score: r.tasks_completed * 10 + r.reviews_done * 5 + r.decisions_made * 3 + r.kb_contributions * 2 + r.bugs_found * 8,
|
|
132
|
+
tasks_completed: r.tasks_completed,
|
|
133
|
+
reviews_done: r.reviews_done,
|
|
134
|
+
strengths: r.strengths,
|
|
135
|
+
avg_task_time_sec: avgTaskTime,
|
|
136
|
+
messages_sent: r.messages_sent,
|
|
137
|
+
last_active: r.last_active,
|
|
138
|
+
};
|
|
139
|
+
}).sort((a, b) => b.score - a.score);
|
|
140
|
+
|
|
141
|
+
return { leaderboard, total_agents: leaderboard.length };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// --- MCP tool definitions ---
|
|
145
|
+
|
|
146
|
+
const definitions = [
|
|
147
|
+
{
|
|
148
|
+
name: 'update_profile',
|
|
149
|
+
description: 'Update your agent profile (display name, avatar, bio, role). Profile data is shown in the dashboard.',
|
|
150
|
+
inputSchema: {
|
|
151
|
+
type: 'object',
|
|
152
|
+
properties: {
|
|
153
|
+
display_name: { type: 'string', description: 'Display name (max 30 chars)' },
|
|
154
|
+
avatar: { type: 'string', description: 'Avatar URL or data URI (max 64KB)' },
|
|
155
|
+
bio: { type: 'string', description: 'Short bio (max 200 chars)' },
|
|
156
|
+
role: { type: 'string', description: 'Role/title (max 30 chars, e.g. "Architect", "Reviewer")' },
|
|
157
|
+
},
|
|
158
|
+
additionalProperties: false,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: 'workspace_write',
|
|
163
|
+
description: 'Write a key-value entry to your workspace. Other agents can read your workspace but only you can write to it. Max 50 keys, 100KB per value.',
|
|
164
|
+
inputSchema: { type: 'object', properties: { key: { type: 'string', description: 'Key name (1-50 alphanumeric/underscore/hyphen/dot chars)' }, content: { type: 'string', description: 'Content to store (max 100KB)' } }, required: ['key', 'content'], additionalProperties: false },
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: 'workspace_read',
|
|
168
|
+
description: 'Read workspace entries. Read your own or another agent\'s workspace. Omit key to read all entries.',
|
|
169
|
+
inputSchema: { type: 'object', properties: { key: { type: 'string', description: 'Specific key to read (optional — omit for all keys)' }, agent: { type: 'string', description: 'Agent whose workspace to read (optional — defaults to yourself)' } }, additionalProperties: false },
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: 'workspace_list',
|
|
173
|
+
description: 'List workspace keys. Specify agent for one workspace, or omit for all agents\' workspace summaries.',
|
|
174
|
+
inputSchema: { type: 'object', properties: { agent: { type: 'string', description: 'Agent name (optional — omit for all)' } }, additionalProperties: false },
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: 'list_branches',
|
|
178
|
+
description: 'List all conversation branches with message counts and metadata.',
|
|
179
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: 'get_reputation',
|
|
183
|
+
description: 'View agent reputation — tasks completed, reviews done, bugs found, strengths. Shows leaderboard when called without agent name.',
|
|
184
|
+
inputSchema: { type: 'object', properties: { agent: { type: 'string', description: 'Agent name (optional — omit for leaderboard)' } }, additionalProperties: false },
|
|
185
|
+
},
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
const handlers = {
|
|
189
|
+
update_profile: function (args) { return toolUpdateProfile(args.display_name, args.avatar, args.bio, args.role); },
|
|
190
|
+
workspace_write: function (args) { return toolWorkspaceWrite(args.key, args.content); },
|
|
191
|
+
workspace_read: function (args) { return toolWorkspaceRead(args.key, args.agent); },
|
|
192
|
+
workspace_list: function (args) { return toolWorkspaceList(args.agent); },
|
|
193
|
+
list_branches: function () { return toolListBranches(); },
|
|
194
|
+
get_reputation: function (args) { return toolGetReputation(args.agent); },
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
return { definitions, handlers };
|
|
198
|
+
};
|