brainiverse 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +228 -2
- package/assets/brainiverse-intro.svg +120 -0
- package/assets/brainiverse.svg +85 -0
- package/bin/brainiverse.mjs +102 -0
- package/package.json +30 -4
- package/skill/brainiverse/SKILL.md +105 -0
- package/skill/brainiverse/agents/analyst.md +22 -0
- package/skill/brainiverse/agents/challenger.md +23 -0
- package/skill/brainiverse/agents/communicator.md +18 -0
- package/skill/brainiverse/agents/creative-thinker.md +20 -0
- package/skill/brainiverse/agents/executor.md +23 -0
- package/skill/brainiverse/agents/leader.md +39 -0
- package/skill/brainiverse/agents/organizer.md +18 -0
- package/skill/brainiverse/agents/quality-reviewer.md +24 -0
- package/skill/brainiverse/agents/researcher.md +25 -0
- package/skill/brainiverse/agents/specialist.md +19 -0
- package/skill/brainiverse/bin/brainiverse.mjs +5 -0
- package/skill/brainiverse/docs/architecture.md +73 -0
- package/skill/brainiverse/docs/cli.md +628 -0
- package/skill/brainiverse/docs/customization.md +86 -0
- package/skill/brainiverse/docs/examples.md +96 -0
- package/skill/brainiverse/docs/limitations.md +38 -0
- package/skill/brainiverse/meetings/meeting-protocol.md +90 -0
- package/skill/brainiverse/runtime/args.mjs +87 -0
- package/skill/brainiverse/runtime/audit.mjs +26 -0
- package/skill/brainiverse/runtime/briefs.mjs +139 -0
- package/skill/brainiverse/runtime/cli.mjs +859 -0
- package/skill/brainiverse/runtime/data/archetypes.json +165 -0
- package/skill/brainiverse/runtime/data/domains.json +628 -0
- package/skill/brainiverse/runtime/data/templates/app-builders.json +25 -0
- package/skill/brainiverse/runtime/data/templates/discovery-crew.json +24 -0
- package/skill/brainiverse/runtime/data/templates/travel-squad.json +25 -0
- package/skill/brainiverse/runtime/data/terminology.json +18 -0
- package/skill/brainiverse/runtime/data/themes.json +50 -0
- package/skill/brainiverse/runtime/evaluation.mjs +58 -0
- package/skill/brainiverse/runtime/meetings.mjs +224 -0
- package/skill/brainiverse/runtime/memory.mjs +115 -0
- package/skill/brainiverse/runtime/orchestrator.mjs +385 -0
- package/skill/brainiverse/runtime/permissions.mjs +67 -0
- package/skill/brainiverse/runtime/registry.mjs +230 -0
- package/skill/brainiverse/runtime/skills.mjs +204 -0
- package/skill/brainiverse/runtime/store.mjs +216 -0
- package/skill/brainiverse/runtime/teams.mjs +272 -0
- package/skill/brainiverse/runtime/tools.mjs +74 -0
- package/skill/brainiverse/teams/app-builders.md +24 -0
- package/skill/brainiverse/teams/discovery-crew.md +22 -0
- package/skill/brainiverse/teams/travel-squad.md +26 -0
- package/skill/brainiverse/workflows/collaboration.md +26 -0
- package/skill/brainiverse/workflows/mission-intake.md +35 -0
- package/skill/brainiverse/workflows/orchestration.md +59 -0
- package/skill/brainiverse/workflows/skill-discovery.md +61 -0
- package/skill/brainiverse/workflows/team-creation.md +41 -0
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
// Command-line integration layer. The Leader (the main Claude Code session) drives everything
|
|
2
|
+
// through these commands; subagents may only read their brief and submit their result.
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import * as audit from "./audit.mjs";
|
|
5
|
+
import * as briefs from "./briefs.mjs";
|
|
6
|
+
import * as evaluation from "./evaluation.mjs";
|
|
7
|
+
import * as meetings from "./meetings.mjs";
|
|
8
|
+
import * as memory from "./memory.mjs";
|
|
9
|
+
import * as orchestrator from "./orchestrator.mjs";
|
|
10
|
+
import * as permissions from "./permissions.mjs";
|
|
11
|
+
import * as registry from "./registry.mjs";
|
|
12
|
+
import * as skills from "./skills.mjs";
|
|
13
|
+
import * as teams from "./teams.mjs";
|
|
14
|
+
import * as tools from "./tools.mjs";
|
|
15
|
+
import { HelpRequested, parse } from "./args.mjs";
|
|
16
|
+
import { BrainiverseError, Workspace, readJson, writeJson } from "./store.mjs";
|
|
17
|
+
|
|
18
|
+
// [command, subcommand] pairs a subagent may run. Everything else is Leader-only.
|
|
19
|
+
const AGENT_ALLOWED = new Set([
|
|
20
|
+
"task brief", "task submit", "task show", "task list", "status", "meeting show", "chat show",
|
|
21
|
+
"skill local", "skill scan", "archetypes", "memory list",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const splitCsv = (value) => (value || "").split(",").map((v) => v.trim()).filter(Boolean);
|
|
25
|
+
|
|
26
|
+
function readPayload(args) {
|
|
27
|
+
let raw = (args.file ? fs.readFileSync(args.file, "utf8") : fs.readFileSync(0, "utf8")).trim();
|
|
28
|
+
if (raw.startsWith("```")) raw = raw.replace(/^```[a-z]*\n?/, "").replace(/```$/, "");
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(raw);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
throw new BrainiverseError(`result is not valid JSON: ${err.message}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseLimits(items = []) {
|
|
37
|
+
const limits = {};
|
|
38
|
+
for (const item of items) {
|
|
39
|
+
const [k, v] = item.split("=");
|
|
40
|
+
if (!/^\d+$/.test(v || "")) throw new BrainiverseError(`limit ${k} must be a non-negative integer`);
|
|
41
|
+
limits[k] = Number(v);
|
|
42
|
+
}
|
|
43
|
+
return limits;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// -- team loading: a saved team, or the active mission's snapshot ("@mission") ------------
|
|
47
|
+
|
|
48
|
+
const loadTeam = (ws, ref, mission) => (ref === "@mission" ? ws.readMission(mission).team : teams.load(ws, ref));
|
|
49
|
+
|
|
50
|
+
function storeTeam(ws, ref, team, mission) {
|
|
51
|
+
if (ref === "@mission") {
|
|
52
|
+
ws.mission(mission, (state) => { state.team = teams.validateTeam(team); });
|
|
53
|
+
return team;
|
|
54
|
+
}
|
|
55
|
+
return teams.save(ws, team);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const loadAgent = (ws, a) => (a.team ? teams.findAgent(loadTeam(ws, a.team, a.mission), a.agent_id) : registry.load(ws, a.agent_id));
|
|
59
|
+
|
|
60
|
+
function storeAgent(ws, a, profile) {
|
|
61
|
+
if (a.team) storeTeam(ws, a.team, teams.replaceAgent(loadTeam(ws, a.team, a.mission), profile), a.mission);
|
|
62
|
+
else registry.save(ws, profile);
|
|
63
|
+
return profile;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function addToTeamOrLibrary(ws, a, profile) {
|
|
67
|
+
if (a.team) storeTeam(ws, a.team, teams.addAgent(loadTeam(ws, a.team, a.mission), profile), a.mission);
|
|
68
|
+
else registry.save(ws, profile);
|
|
69
|
+
return profile;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// -- handlers ---------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
const handlers = {};
|
|
75
|
+
|
|
76
|
+
handlers.init = (ws, a) => {
|
|
77
|
+
if (a.theme && !(a.theme in registry.themes())) throw new BrainiverseError(`unknown theme ${a.theme}`);
|
|
78
|
+
if (a.terminology) registry.terminology(a.terminology);
|
|
79
|
+
return { workspace: ws.root, config: ws.init({ theme: a.theme, terminology: a.terminology }) };
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
handlers.config = (ws, a) => {
|
|
83
|
+
if (a.key === undefined) return ws.config();
|
|
84
|
+
if (a.value === undefined) return { [a.key]: ws.config()[a.key] };
|
|
85
|
+
if (!["theme", "terminology", "host"].includes(a.key)) throw new BrainiverseError("settable keys: theme, terminology, host");
|
|
86
|
+
if (a.key === "theme" && !(a.value in registry.themes())) throw new BrainiverseError(`unknown theme ${a.value}`);
|
|
87
|
+
if (a.key === "terminology") registry.terminology(a.value);
|
|
88
|
+
if (a.key === "host" && !(a.value in tools.TOOL_ADAPTERS)) throw new BrainiverseError(`unknown host ${a.value}`);
|
|
89
|
+
return ws.setConfig({ [a.key]: a.value });
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
handlers.archetypes = () => Object.fromEntries(Object.entries(registry.archetypes())
|
|
93
|
+
.map(([k, v]) => [k, { title: v.title, purpose: v.purpose, capabilities: v.capabilities }]));
|
|
94
|
+
|
|
95
|
+
handlers.themes = (ws) => {
|
|
96
|
+
const config = ws.config();
|
|
97
|
+
return {
|
|
98
|
+
active: config.theme, terminology: registry.terminology(config.terminology),
|
|
99
|
+
themes: Object.fromEntries(Object.entries(registry.themes()).map(([k, v]) => [k, {
|
|
100
|
+
description: v.description, names: Object.fromEntries(Object.entries(v.identities).map(([arch, i]) => [arch, i.name])),
|
|
101
|
+
}])),
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
handlers.capabilities = (ws) => Object.fromEntries(Object.entries(tools.CAPABILITIES).map(([c, info]) => [c, {
|
|
106
|
+
...info, approval_per_use: tools.isGated(c), host_tools: tools.hostTools([c], ws.config().host),
|
|
107
|
+
}]));
|
|
108
|
+
|
|
109
|
+
handlers.agent = (ws, a, sub) => {
|
|
110
|
+
if (sub === "list") {
|
|
111
|
+
if (a.team) {
|
|
112
|
+
return loadTeam(ws, a.team, a.mission).agents.map((p) => ({ id: p.id, name: p.name, archetype: p.archetype, enabled: p.enabled }));
|
|
113
|
+
}
|
|
114
|
+
return registry.listAgents(ws).map((p) => ({ id: p.id, name: p.name, archetype: p.archetype }));
|
|
115
|
+
}
|
|
116
|
+
if (sub === "new") {
|
|
117
|
+
let profile = registry.makeAgent(a.archetype, { id: a.id, theme: ws.config().theme, name: a.name });
|
|
118
|
+
if (a.set) profile = registry.edit(profile, { sets: a.set });
|
|
119
|
+
return addToTeamOrLibrary(ws, a, profile);
|
|
120
|
+
}
|
|
121
|
+
if (sub === "import") {
|
|
122
|
+
const [profile, stripped] = registry.sanitizeImport(readJson(a.path));
|
|
123
|
+
addToTeamOrLibrary(ws, a, profile);
|
|
124
|
+
return { imported: profile.id, stripped_gated_capabilities: stripped,
|
|
125
|
+
note: stripped.length ? "Gated capabilities are never imported; grant them with an approval." : "" };
|
|
126
|
+
}
|
|
127
|
+
const profile = loadAgent(ws, a);
|
|
128
|
+
switch (sub) {
|
|
129
|
+
case "show": return { ...profile, host_tools: tools.hostTools(profile.capabilities, ws.config().host) };
|
|
130
|
+
case "edit": return storeAgent(ws, a, registry.edit(profile, { sets: a.set || [], adds: a.add || [], removes: a.remove || [] }));
|
|
131
|
+
case "rename": return storeAgent(ws, a, registry.rename(profile, {
|
|
132
|
+
name: a.name, personality: a.personality, communication_style: a.style, avatar: a.avatar }));
|
|
133
|
+
case "enable":
|
|
134
|
+
case "disable": return storeAgent(ws, a, registry.validateAgent({ ...profile, enabled: sub === "enable" }));
|
|
135
|
+
case "duplicate": return addToTeamOrLibrary(ws, a, registry.duplicate(profile, a.new_id, a.name));
|
|
136
|
+
case "delete":
|
|
137
|
+
if (a.team) storeTeam(ws, a.team, teams.removeAgent(loadTeam(ws, a.team, a.mission), profile.id), a.mission);
|
|
138
|
+
else ws.deleteRecord("agents", profile.id);
|
|
139
|
+
return { deleted: profile.id };
|
|
140
|
+
case "export": {
|
|
141
|
+
const out = a.out || `${profile.id}.agent.json`;
|
|
142
|
+
writeJson(out, profile);
|
|
143
|
+
return { exported: out };
|
|
144
|
+
}
|
|
145
|
+
case "grant": {
|
|
146
|
+
if (!tools.isGated(a.capability)) return storeAgent(ws, a, registry.grant(profile, a.capability));
|
|
147
|
+
const updated = ws.mission(a.mission, (state) => {
|
|
148
|
+
const approval = permissions.getApproval(state, a.approval, "capability");
|
|
149
|
+
const granted = registry.grant(profile, a.capability, approval);
|
|
150
|
+
permissions.consume(approval);
|
|
151
|
+
audit.log(ws, state.id, "capability_granted", { agent: profile.id, capability: a.capability, approval: approval.id });
|
|
152
|
+
return granted;
|
|
153
|
+
});
|
|
154
|
+
return storeAgent(ws, a, updated);
|
|
155
|
+
}
|
|
156
|
+
case "revoke": return storeAgent(ws, a, registry.revoke(profile, a.capability));
|
|
157
|
+
case "test": {
|
|
158
|
+
const model = tools.hostModel(profile.model, ws.config().host);
|
|
159
|
+
return {
|
|
160
|
+
note: "Run this prompt with the Agent tool to test the agent in isolation; nothing is recorded.",
|
|
161
|
+
agent_call: {
|
|
162
|
+
subagent_type: "general-purpose", description: `Test ${profile.name}`, ...(model ? { model } : {}),
|
|
163
|
+
prompt: `You are ${profile.name}, a ${registry.archetypes()[profile.archetype].title}. ` +
|
|
164
|
+
`Personality: ${profile.personality}. Responsibilities: ${profile.responsibilities.join("; ")}. ${profile.instructions}\n\n` +
|
|
165
|
+
`Test task: ${a.task}\n\nUse only these tools: ${tools.hostTools(profile.capabilities).join(", ") || "none"}. ` +
|
|
166
|
+
"Answer with the result-protocol fields: status, findings, evidence, assumptions, uncertainty, recommendations.",
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
throw new BrainiverseError(`unknown agent command ${sub}`);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
handlers.team = (ws, a, sub) => {
|
|
175
|
+
const theme = ws.config().theme;
|
|
176
|
+
if (sub === "templates") {
|
|
177
|
+
return teams.templateIds().map((id) => {
|
|
178
|
+
const t = teams.loadTemplate(id);
|
|
179
|
+
return { id, name: t.name, description: t.description };
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
if (sub === "list") {
|
|
183
|
+
return ws.listRecords("teams").map((t) => ({ id: t.id, name: t.name, agents: t.agents.length, status: t.status || "approved" }));
|
|
184
|
+
}
|
|
185
|
+
if (sub === "propose") {
|
|
186
|
+
const proposal = teams.proposeTeam(a.mission_text, { theme, maxAgents: a.max, teamId: a.id });
|
|
187
|
+
const { team, reasons, ...rest } = proposal;
|
|
188
|
+
team.status = "draft";
|
|
189
|
+
teams.save(ws, team);
|
|
190
|
+
return {
|
|
191
|
+
...rest, team_id: team.id,
|
|
192
|
+
roster: team.agents.map((p) => ({ id: p.id, name: p.name, archetype: p.archetype, why: reasons[p.id], capabilities: p.capabilities })),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (sub === "from-template") {
|
|
196
|
+
const team = teams.teamFromTemplate(a.template, { teamId: a.id, theme });
|
|
197
|
+
team.status = "draft";
|
|
198
|
+
return teams.save(ws, team);
|
|
199
|
+
}
|
|
200
|
+
if (sub === "import") {
|
|
201
|
+
const [team, stripped] = teams.sanitizeImport(readJson(a.path));
|
|
202
|
+
teams.save(ws, team);
|
|
203
|
+
return { imported: team.id, stripped_gated_capabilities: stripped };
|
|
204
|
+
}
|
|
205
|
+
const team = loadTeam(ws, a.team_ref, a.mission);
|
|
206
|
+
switch (sub) {
|
|
207
|
+
case "show": return { ...team, warnings: teams.redundancyWarnings(team) };
|
|
208
|
+
case "approve": return storeTeam(ws, a.team_ref, { ...team, status: "approved" }, a.mission).status;
|
|
209
|
+
case "add": {
|
|
210
|
+
const profile = a.from_library ? registry.load(ws, a.from_library) : registry.makeAgent(a.archetype, {
|
|
211
|
+
id: a.id, theme, name: a.name, description: a.description, expertise: a.expertise ? splitCsv(a.expertise) : undefined,
|
|
212
|
+
});
|
|
213
|
+
const updated = teams.addAgent(team, profile);
|
|
214
|
+
storeTeam(ws, a.team_ref, updated, a.mission);
|
|
215
|
+
return { added: profile.id, warnings: teams.redundancyWarnings(updated) };
|
|
216
|
+
}
|
|
217
|
+
case "remove":
|
|
218
|
+
storeTeam(ws, a.team_ref, teams.removeAgent(team, a.agent_id), a.mission);
|
|
219
|
+
return { removed: a.agent_id };
|
|
220
|
+
case "leader": return storeTeam(ws, a.team_ref, teams.setLeader(team, a.agent_id), a.mission).leader;
|
|
221
|
+
case "rename": return storeTeam(ws, a.team_ref, { ...team, name: a.name }, a.mission).name;
|
|
222
|
+
case "save": {
|
|
223
|
+
const saved = teams.save(ws, { ...team, id: a.as || team.id, status: "approved" });
|
|
224
|
+
return { saved: saved.id };
|
|
225
|
+
}
|
|
226
|
+
case "export": {
|
|
227
|
+
const out = a.out || `${team.id}.team.json`;
|
|
228
|
+
writeJson(out, team);
|
|
229
|
+
return { exported: out };
|
|
230
|
+
}
|
|
231
|
+
case "delete":
|
|
232
|
+
ws.deleteRecord("teams", team.id);
|
|
233
|
+
return { deleted: team.id };
|
|
234
|
+
}
|
|
235
|
+
throw new BrainiverseError(`unknown team command ${sub}`);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
handlers.mission = (ws, a, sub) => {
|
|
239
|
+
switch (sub) {
|
|
240
|
+
case "new": {
|
|
241
|
+
const team = teams.load(ws, a.team);
|
|
242
|
+
const state = orchestrator.newMission(a.goal, team, {
|
|
243
|
+
id: a.id, title: a.title, success: a.success || [], constraints: a.constraint || [],
|
|
244
|
+
assumptions: a.assumption || [], resources: a.resource || [], limits: parseLimits(a.limit),
|
|
245
|
+
});
|
|
246
|
+
ws.createMission(state);
|
|
247
|
+
ws.setConfig({ active_mission: state.id });
|
|
248
|
+
audit.log(ws, state.id, "mission_created", { goal: state.goal, team: team.id });
|
|
249
|
+
return { mission: state.id, team: team.id, limits: state.limits };
|
|
250
|
+
}
|
|
251
|
+
case "list": {
|
|
252
|
+
const active = ws.config().active_mission;
|
|
253
|
+
return ws.listMissions().map((m) => ({ id: m.id, title: m.title, status: m.status, active: m.id === active }));
|
|
254
|
+
}
|
|
255
|
+
case "use":
|
|
256
|
+
ws.resolveMission(a.mission_id);
|
|
257
|
+
return ws.setConfig({ active_mission: a.mission_id }).active_mission;
|
|
258
|
+
case "show": {
|
|
259
|
+
const s = ws.readMission(a.mission);
|
|
260
|
+
const keys = ["id", "title", "goal", "status", "success_criteria", "constraints", "assumptions", "resources", "limits", "decisions", "usage"];
|
|
261
|
+
return Object.fromEntries(keys.map((k) => [k, s[k]]));
|
|
262
|
+
}
|
|
263
|
+
case "pause":
|
|
264
|
+
case "resume":
|
|
265
|
+
case "close": {
|
|
266
|
+
const target = { pause: "paused", resume: "active", close: "closed" }[sub];
|
|
267
|
+
return ws.mission(a.mission, (state) => {
|
|
268
|
+
if (sub === "close") {
|
|
269
|
+
const open = Object.values(state.tasks).filter((t) => ["running", "submitted"].includes(t.status)).map((t) => t.id);
|
|
270
|
+
if (open.length && !a.force) throw new BrainiverseError(`tasks still open: ${open.join(", ")}; finish them or pass --force`);
|
|
271
|
+
}
|
|
272
|
+
state.status = target;
|
|
273
|
+
audit.log(ws, state.id, `mission_${target}`);
|
|
274
|
+
return { mission: state.id, status: target };
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
case "update":
|
|
278
|
+
return ws.mission(a.mission, (state) => {
|
|
279
|
+
for (const [field, values] of [["success_criteria", a.success], ["constraints", a.constraint],
|
|
280
|
+
["assumptions", a.assumption], ["resources", a.resource]]) state[field].push(...(values || []));
|
|
281
|
+
for (const [k, v] of Object.entries(parseLimits(a.limit))) {
|
|
282
|
+
if (!(k in orchestrator.DEFAULT_LIMITS)) throw new BrainiverseError(`unknown limit ${k}`);
|
|
283
|
+
state.limits[k] = v;
|
|
284
|
+
}
|
|
285
|
+
if (a.limit) audit.log(ws, state.id, "limits_changed", { limits: a.limit });
|
|
286
|
+
const { success_criteria, constraints, assumptions, resources, limits } = state;
|
|
287
|
+
return { success_criteria, constraints, assumptions, resources, limits };
|
|
288
|
+
});
|
|
289
|
+
case "finalize":
|
|
290
|
+
return ws.mission(a.mission, (state) => {
|
|
291
|
+
state.final_report = fs.readFileSync(a.file, "utf8");
|
|
292
|
+
audit.log(ws, state.id, "final_report_saved");
|
|
293
|
+
return { saved: true };
|
|
294
|
+
});
|
|
295
|
+
case "report": return report(ws.readMission(a.mission));
|
|
296
|
+
case "log": return audit.read(ws, ws.resolveMission(a.mission), a.limit);
|
|
297
|
+
}
|
|
298
|
+
throw new BrainiverseError(`unknown mission command ${sub}`);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
export function report(state) {
|
|
302
|
+
const names = Object.fromEntries(state.team.agents.map((p) => [p.id, p.name]));
|
|
303
|
+
const tasks = Object.values(state.tasks);
|
|
304
|
+
const lines = [`# Mission report: ${state.title}`, "", `**Goal:** ${state.goal}`, `**Status:** ${state.status}`, ""];
|
|
305
|
+
if (state.success_criteria.length) lines.push("## Success criteria", ...state.success_criteria.map((c) => `- ${c}`), "");
|
|
306
|
+
lines.push("## Deliverables (accepted work)");
|
|
307
|
+
for (const t of tasks.filter((t) => t.status === "done" && t.kind === "task")) {
|
|
308
|
+
const f = t.result.findings;
|
|
309
|
+
const text = typeof f === "string" ? f : f.map(String).join("; ");
|
|
310
|
+
lines.push(`- **${t.id}** (${names[t.agent]}): ${t.objective}\n ${text.slice(0, 600)}`);
|
|
311
|
+
for (const d of t.result.deliverables || []) lines.push(` - file: ${d.path} — ${d.description || ""}`);
|
|
312
|
+
}
|
|
313
|
+
lines.push("");
|
|
314
|
+
const unfinished = tasks.filter((t) => t.status !== "done");
|
|
315
|
+
if (unfinished.length) {
|
|
316
|
+
lines.push("## Outstanding or unsuccessful work",
|
|
317
|
+
...unfinished.map((t) => `- ${t.id} [${t.status}] ${t.objective}${t.errors.length ? ` — ${t.errors.at(-1)}` : ""}`), "");
|
|
318
|
+
}
|
|
319
|
+
if (state.decisions.length) {
|
|
320
|
+
lines.push("## Decisions", ...state.decisions.map((d) => `- ${d.id}: ${d.text}${d.approved_by_human ? " (approved by human)" : ""}`), "");
|
|
321
|
+
}
|
|
322
|
+
const assumptions = [...state.assumptions];
|
|
323
|
+
for (const t of tasks) if (t.status === "done" && t.result) assumptions.push(...(t.result.assumptions || []).map((x) => `${x} (${t.id})`));
|
|
324
|
+
if (assumptions.length) lines.push("## Assumptions", ...assumptions.map((x) => `- ${x}`), "");
|
|
325
|
+
const issues = state.reviews.flatMap((r) => r.issues.map((i) => `${r.target}: ${i}`));
|
|
326
|
+
const openQ = Object.values(state.meetings).flatMap((m) => m.questions.filter((q) => !q.answer).map((q) => `${m.id}: ${q.text}`));
|
|
327
|
+
if (issues.length || openQ.length) lines.push("## Unresolved issues and questions", ...[...issues, ...openQ].map((x) => `- ${x}`), "");
|
|
328
|
+
if (state.reviews.length) {
|
|
329
|
+
lines.push("## Reviews", ...state.reviews.map((r) =>
|
|
330
|
+
`- ${r.target} by ${names[r.reviewer] || r.reviewer}: **${r.verdict}** — ${r.evidence.join("; ").slice(0, 300)}`), "");
|
|
331
|
+
}
|
|
332
|
+
const integrated = Object.values(state.skills.candidates).filter((c) => c.status === "integrated");
|
|
333
|
+
if (integrated.length) lines.push("## Skills integrated", ...integrated.map((c) => `- ${c.name} → ${names[c.agent]} (${c.installed_path})`), "");
|
|
334
|
+
const m = evaluation.metrics(state);
|
|
335
|
+
lines.push("## Metrics", `- Tasks done: ${m.tasks_done}/${m.tasks_total}, failed: ${m.tasks_failed}`,
|
|
336
|
+
`- Agent invocations: ${m.invocations} (unused: ${m.unused_invocations}, retries: ${m.retries}, revisions: ${m.revisions})`,
|
|
337
|
+
`- Idle team members: ${m.idle_team_members.map((x) => names[x] || x).join(", ") || "none"}`);
|
|
338
|
+
if (state.final_report) lines.push("", "## Leader's synthesis", state.final_report);
|
|
339
|
+
return lines.join("\n");
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const startPayload = (ws, state, task, invocation) => ({
|
|
343
|
+
task: task.id, invocation: invocation.id, agent: task.agent, agent_call: briefs.agentCall(ws, state, task),
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
handlers.task = (ws, a, sub) => {
|
|
347
|
+
const acting = permissions.actingAgent();
|
|
348
|
+
if (sub === "list") {
|
|
349
|
+
let rows = Object.values(ws.readMission(a.mission).tasks);
|
|
350
|
+
if (a.status) rows = rows.filter((t) => t.status === a.status);
|
|
351
|
+
return rows.map((t) => ({ id: t.id, agent: t.agent, status: t.status, objective: t.objective.slice(0, 90), depends_on: t.depends_on, kind: t.kind }));
|
|
352
|
+
}
|
|
353
|
+
if (sub === "show") return orchestrator.getTask(ws.readMission(a.mission), a.task_id);
|
|
354
|
+
if (sub === "brief") {
|
|
355
|
+
const state = ws.readMission(a.mission);
|
|
356
|
+
const task = orchestrator.getTask(state, a.task_id);
|
|
357
|
+
if (acting && acting !== task.agent) throw new BrainiverseError(`this brief belongs to ${task.agent}, not ${acting}`);
|
|
358
|
+
return briefs.build(ws, state, task);
|
|
359
|
+
}
|
|
360
|
+
return ws.mission(a.mission, (state) => {
|
|
361
|
+
switch (sub) {
|
|
362
|
+
case "ready": return orchestrator.readyTasks(state).map((t) => ({ id: t.id, agent: t.agent, objective: t.objective }));
|
|
363
|
+
case "add": {
|
|
364
|
+
const task = orchestrator.addTask(state, {
|
|
365
|
+
objective: a.objective, agent: a.agent, inputs: a.input || [], expected_output: a.expect || "",
|
|
366
|
+
constraints: a.constraint || [], depends_on: splitCsv(a.after), acceptance: a.accept || [], context: a.context || "",
|
|
367
|
+
needs: splitCsv(a.needs), timeout_s: a.timeout, max_retries: a.retries, parent: a.parent || null,
|
|
368
|
+
});
|
|
369
|
+
audit.log(ws, state.id, "task_added", { task: task.id, agent: task.agent, objective: task.objective });
|
|
370
|
+
return { task: task.id, status: task.status };
|
|
371
|
+
}
|
|
372
|
+
case "start": {
|
|
373
|
+
const ids = a.task_id === "ready" ? orchestrator.readyTasks(state).map((t) => t.id) : [a.task_id];
|
|
374
|
+
if (!ids.length) throw new BrainiverseError("no task is ready to start");
|
|
375
|
+
const started = ids.map((id) => {
|
|
376
|
+
const invocation = orchestrator.startTask(state, id);
|
|
377
|
+
audit.log(ws, state.id, "task_started", { task: id, invocation: invocation.id, agent: invocation.agent });
|
|
378
|
+
return startPayload(ws, state, state.tasks[id], invocation);
|
|
379
|
+
});
|
|
380
|
+
return { started, next: "Invoke every agent_call with the Agent tool. Calls for different tasks are independent: " +
|
|
381
|
+
"send them in one message to run them in parallel." };
|
|
382
|
+
}
|
|
383
|
+
case "submit": {
|
|
384
|
+
const task = orchestrator.getTask(state, a.task_id);
|
|
385
|
+
if (acting && acting !== task.agent) throw new BrainiverseError(`${acting} cannot submit ${task.agent}'s task`);
|
|
386
|
+
const result = readPayload(a);
|
|
387
|
+
orchestrator.submit(state, a.task_id, result, { tokens: a.tokens || 0 });
|
|
388
|
+
if (task.kind === "meeting") meetings.recordContribution(state, task);
|
|
389
|
+
audit.log(ws, state.id, "task_submitted", { task: task.id, result_status: result.status }, acting || "leader");
|
|
390
|
+
return { task: task.id, status: task.status, submitted_by: acting || "leader" };
|
|
391
|
+
}
|
|
392
|
+
case "accept": {
|
|
393
|
+
const task = orchestrator.accept(state, a.task_id, a.evidence || "");
|
|
394
|
+
audit.log(ws, state.id, "task_accepted", { task: task.id, evidence: a.evidence || "" });
|
|
395
|
+
return { task: task.id, status: task.status };
|
|
396
|
+
}
|
|
397
|
+
case "revise": {
|
|
398
|
+
const task = orchestrator.revise(state, a.task_id, a.feedback);
|
|
399
|
+
audit.log(ws, state.id, "task_revision", { task: task.id, feedback: a.feedback });
|
|
400
|
+
return { task: task.id, status: task.status, iteration: task.iterations };
|
|
401
|
+
}
|
|
402
|
+
case "fail": {
|
|
403
|
+
const task = orchestrator.fail(state, a.task_id, a.error);
|
|
404
|
+
audit.log(ws, state.id, "task_failed", { task: task.id, error: a.error, now: task.status });
|
|
405
|
+
return { task: task.id, status: task.status, attempts: task.attempts, retrying: task.status === "pending" };
|
|
406
|
+
}
|
|
407
|
+
case "cancel": {
|
|
408
|
+
const task = orchestrator.cancel(state, a.task_id, a.reason || "cancelled by the human");
|
|
409
|
+
audit.log(ws, state.id, "task_cancelled", { task: task.id });
|
|
410
|
+
return { task: task.id, status: "cancelled", blocked: Object.values(state.tasks).filter((t) => t.status === "blocked").map((t) => t.id) };
|
|
411
|
+
}
|
|
412
|
+
case "sweep": {
|
|
413
|
+
const timedOut = orchestrator.sweepTimeouts(state);
|
|
414
|
+
for (const id of timedOut) audit.log(ws, state.id, "task_timeout", { task: id });
|
|
415
|
+
return { timed_out: timedOut };
|
|
416
|
+
}
|
|
417
|
+
case "reassign": {
|
|
418
|
+
const task = orchestrator.reassign(state, a.task_id, a.agent);
|
|
419
|
+
audit.log(ws, state.id, "task_reassigned", { task: task.id, agent: a.agent });
|
|
420
|
+
return { task: task.id, agent: task.agent };
|
|
421
|
+
}
|
|
422
|
+
case "attach-approval": {
|
|
423
|
+
const task = orchestrator.getTask(state, a.task_id);
|
|
424
|
+
const approval = permissions.getApproval(state, a.approval);
|
|
425
|
+
if (approval.status !== "approved") throw new BrainiverseError(`${approval.id} is ${approval.status}`);
|
|
426
|
+
task.approval = approval.id;
|
|
427
|
+
return { task: task.id, approval: approval.id };
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
throw new BrainiverseError(`unknown task command ${sub}`);
|
|
431
|
+
});
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
handlers.status = (ws, a) => {
|
|
435
|
+
const state = ws.readMission(a.mission);
|
|
436
|
+
const p = orchestrator.progress(state);
|
|
437
|
+
const term = registry.terminology(ws.config().terminology);
|
|
438
|
+
const names = Object.fromEntries(state.team.agents.map((x) => [x.id, x.name]));
|
|
439
|
+
const lines = [
|
|
440
|
+
`${term.project}: ${state.title} [${state.status}]`,
|
|
441
|
+
`${term.team}: ${state.team.name} — ${state.team.agents.map((x) => `${x.name}${x.enabled ? "" : " (off)"}`).join(", ")}`,
|
|
442
|
+
`Tasks: ${Object.entries(p.tasks).filter(([, v]) => v).map(([k, v]) => `${k} ${v}`).join(", ") || "none yet"}`,
|
|
443
|
+
`Invocations: ${p.invocations} Tokens: ${p.tokens}`,
|
|
444
|
+
];
|
|
445
|
+
for (const t of Object.values(state.tasks)) {
|
|
446
|
+
if (!["done", "cancelled"].includes(t.status)) {
|
|
447
|
+
lines.push(` ${t.id.padEnd(4)} ${t.status.padEnd(10)} ${(names[t.agent] || t.agent).padEnd(16)} ${t.objective.slice(0, 70)}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (p.ready.length) lines.push(`Ready to start: ${p.ready.join(", ")}`);
|
|
451
|
+
if (p.awaiting_review.length) lines.push(`Awaiting your review/accept: ${p.awaiting_review.join(", ")}`);
|
|
452
|
+
if (p.pending_approvals.length) lines.push(`Pending human approvals: ${p.pending_approvals.join(", ")}`);
|
|
453
|
+
for (const q of p.open_questions) lines.push(`Open question (${q.meeting}): ${q.question}`);
|
|
454
|
+
for (const m of Object.values(state.meetings).filter((m) => ["active", "paused"].includes(m.status))) {
|
|
455
|
+
lines.push(m.mode === "direct"
|
|
456
|
+
? `Direct line ${m.id} [${m.status}] with ${names[m.participants[0]]}`
|
|
457
|
+
: `${term.meeting} ${m.id} [${m.status}]: ${m.objective}`);
|
|
458
|
+
}
|
|
459
|
+
return lines.join("\n");
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
handlers.meeting = (ws, a, sub) => {
|
|
463
|
+
if (sub === "show") {
|
|
464
|
+
const state = ws.readMission(a.mission);
|
|
465
|
+
const m = meetings.getMeeting(state, a.meeting_id);
|
|
466
|
+
const { id, mode, objective, status, participants, decisions, assumptions, questions, actions, summary } = m;
|
|
467
|
+
return { id, mode, objective, status, participants, decisions, assumptions, questions, actions, summary,
|
|
468
|
+
transcript: meetings.transcript(state, a.meeting_id, 200) };
|
|
469
|
+
}
|
|
470
|
+
return ws.mission(a.mission, (state) => {
|
|
471
|
+
switch (sub) {
|
|
472
|
+
case "start": {
|
|
473
|
+
const m = meetings.start(state, a.objective, splitCsv(a.with));
|
|
474
|
+
audit.log(ws, state.id, "meeting_started", { meeting: m.id, participants: m.participants });
|
|
475
|
+
return { meeting: m.id, participants: m.participants };
|
|
476
|
+
}
|
|
477
|
+
case "ask": {
|
|
478
|
+
const created = splitCsv(a.agent).map((id) => meetings.ask(state, a.meeting_id, id, a.prompt).id);
|
|
479
|
+
return { tasks: created, next: "Start them with `task start ready` and invoke the agents in parallel; " +
|
|
480
|
+
"their submissions appear in the transcript." };
|
|
481
|
+
}
|
|
482
|
+
case "say": return meetings.say(state, a.meeting_id, a.text);
|
|
483
|
+
case "human": {
|
|
484
|
+
const message = meetings.human(state, a.meeting_id, a.text, a.answers);
|
|
485
|
+
audit.log(ws, state.id, "human_input", { meeting: a.meeting_id, text: a.text });
|
|
486
|
+
return message;
|
|
487
|
+
}
|
|
488
|
+
case "question": return meetings.question(state, a.meeting_id, a.text, a.to);
|
|
489
|
+
case "decide": {
|
|
490
|
+
const d = meetings.decide(state, a.meeting_id, a.text, Boolean(a.human_approved));
|
|
491
|
+
audit.log(ws, state.id, "decision", { meeting: a.meeting_id, text: a.text, approved_by_human: Boolean(a.human_approved) });
|
|
492
|
+
return d;
|
|
493
|
+
}
|
|
494
|
+
case "assume": return meetings.assume(state, a.meeting_id, a.text);
|
|
495
|
+
case "action":
|
|
496
|
+
return { task: meetings.action(state, a.meeting_id, a.agent, a.objective, { acceptance: a.accept || [], depends_on: splitCsv(a.after) }).id };
|
|
497
|
+
case "add":
|
|
498
|
+
meetings.addParticipant(state, a.meeting_id, a.agent);
|
|
499
|
+
return { participants: state.meetings[a.meeting_id].participants };
|
|
500
|
+
case "remove":
|
|
501
|
+
meetings.removeParticipant(state, a.meeting_id, a.agent);
|
|
502
|
+
return { participants: state.meetings[a.meeting_id].participants };
|
|
503
|
+
case "pause":
|
|
504
|
+
case "resume":
|
|
505
|
+
case "end": {
|
|
506
|
+
const target = { pause: "paused", resume: "active", end: "ended" }[sub];
|
|
507
|
+
const m = meetings.setStatus(state, a.meeting_id, target, a.summary || "");
|
|
508
|
+
audit.log(ws, state.id, `meeting_${target}`, { meeting: m.id });
|
|
509
|
+
return { meeting: m.id, status: m.status };
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
throw new BrainiverseError(`unknown meeting command ${sub}`);
|
|
513
|
+
});
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// Direct line: the human talks one-on-one with a single agent.
|
|
517
|
+
handlers.chat = (ws, a, sub) => {
|
|
518
|
+
if (sub === "list") {
|
|
519
|
+
return Object.values(ws.readMission(a.mission).meetings).filter((m) => m.mode === "direct")
|
|
520
|
+
.map((m) => ({ id: m.id, agent: m.participants[0], status: m.status, topic: m.objective }));
|
|
521
|
+
}
|
|
522
|
+
if (sub === "show") return meetings.transcript(ws.readMission(a.mission), a.chat_id, 200);
|
|
523
|
+
return ws.mission(a.mission, (state) => {
|
|
524
|
+
switch (sub) {
|
|
525
|
+
case "start": {
|
|
526
|
+
if (a.agent === state.team.leader) throw new BrainiverseError("the leader is already your main contact; just talk to them");
|
|
527
|
+
const m = meetings.startDirect(state, a.agent, a.topic || "");
|
|
528
|
+
audit.log(ws, state.id, "direct_line_opened", { chat: m.id, agent: a.agent });
|
|
529
|
+
return { chat: m.id, agent: a.agent };
|
|
530
|
+
}
|
|
531
|
+
case "say": {
|
|
532
|
+
const task = meetings.directMessage(state, a.chat_id, a.text);
|
|
533
|
+
const invocation = orchestrator.startTask(state, task.id);
|
|
534
|
+
audit.log(ws, state.id, "direct_message", { chat: a.chat_id, task: task.id, text: a.text });
|
|
535
|
+
return { ...startPayload(ws, state, task, invocation),
|
|
536
|
+
next: "Invoke the agent_call with the Agent tool, then show the human the agent's reply verbatim " +
|
|
537
|
+
"(`chat show`), attributed to the agent, with no commentary of your own." };
|
|
538
|
+
}
|
|
539
|
+
case "end": {
|
|
540
|
+
const m = meetings.setStatus(state, a.chat_id, "ended", a.summary || "");
|
|
541
|
+
audit.log(ws, state.id, "direct_line_closed", { chat: m.id });
|
|
542
|
+
return { chat: m.id, status: m.status };
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
throw new BrainiverseError(`unknown chat command ${sub}`);
|
|
546
|
+
});
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
handlers.approval = (ws, a, sub) => {
|
|
550
|
+
if (sub === "list") {
|
|
551
|
+
const rows = Object.values(ws.readMission(a.mission).approvals);
|
|
552
|
+
return a.status ? rows.filter((r) => r.status === a.status) : rows;
|
|
553
|
+
}
|
|
554
|
+
return ws.mission(a.mission, (state) => {
|
|
555
|
+
if (sub === "request") {
|
|
556
|
+
const approval = permissions.request(state, { kind: a.kind, detail: a.detail, agent: a.agent || null,
|
|
557
|
+
capability: a.capability || null, task: a.task || null, skill: a.skill || null });
|
|
558
|
+
audit.log(ws, state.id, "approval_requested", { approval: approval.id, detail: a.detail });
|
|
559
|
+
return { ...approval, next: "Ask the human (AskUserQuestion), then record their answer with `approval resolve`." };
|
|
560
|
+
}
|
|
561
|
+
if (sub === "resolve") {
|
|
562
|
+
if (!["approve", "reject"].includes(a.decision)) throw new BrainiverseError("decision must be approve or reject");
|
|
563
|
+
const approval = permissions.resolve(state, a.approval_id, a.decision === "approve", a.note);
|
|
564
|
+
audit.log(ws, state.id, "approval_resolved", { approval: approval.id, status: approval.status, note: a.note });
|
|
565
|
+
return approval;
|
|
566
|
+
}
|
|
567
|
+
throw new BrainiverseError(`unknown approval command ${sub}`);
|
|
568
|
+
});
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
handlers.memory = (ws, a, sub) => {
|
|
572
|
+
const scope = a.scope;
|
|
573
|
+
if (sub === "list") {
|
|
574
|
+
const state = scope === "project" ? ws.readMission(a.mission) : null;
|
|
575
|
+
return memory.list(ws, scope, { state, tags: splitCsv(a.tags) });
|
|
576
|
+
}
|
|
577
|
+
const write = (state) => {
|
|
578
|
+
if (sub === "add") return memory.add(ws, scope, a.text, { tags: splitCsv(a.tags), state });
|
|
579
|
+
if (sub === "edit") return memory.edit(ws, scope, a.record_id, a.text, { state });
|
|
580
|
+
if (sub === "delete") {
|
|
581
|
+
memory.remove(ws, scope, a.record_id, { state });
|
|
582
|
+
return { deleted: a.record_id };
|
|
583
|
+
}
|
|
584
|
+
throw new BrainiverseError(`unknown memory command ${sub}`);
|
|
585
|
+
};
|
|
586
|
+
return scope === "project" ? ws.mission(a.mission, write) : write(null);
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
handlers.skill = (ws, a, sub) => {
|
|
590
|
+
if (sub === "local") {
|
|
591
|
+
let rows = skills.localInventory();
|
|
592
|
+
if (a.query) {
|
|
593
|
+
const words = a.query.toLowerCase().split(/\s+/);
|
|
594
|
+
rows = rows.filter((s) => words.some((w) => `${s.name} ${s.description}`.toLowerCase().includes(w)));
|
|
595
|
+
}
|
|
596
|
+
return rows;
|
|
597
|
+
}
|
|
598
|
+
if (sub === "scan") {
|
|
599
|
+
const result = skills.scan(a.path);
|
|
600
|
+
if (a.candidate) {
|
|
601
|
+
permissions.requireLeader("skill scan --candidate");
|
|
602
|
+
ws.mission(a.mission, (state) => {
|
|
603
|
+
skills.recordScan(state, a.candidate, result);
|
|
604
|
+
audit.log(ws, state.id, "skill_scanned", { skill: a.candidate, risk: result.risk });
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
return result;
|
|
608
|
+
}
|
|
609
|
+
if (sub === "list") {
|
|
610
|
+
const s = ws.readMission(a.mission).skills;
|
|
611
|
+
return { gaps: Object.values(s.gaps), candidates: Object.values(s.candidates) };
|
|
612
|
+
}
|
|
613
|
+
if (sub === "recommend") return skills.recommend(ws.readMission(a.mission));
|
|
614
|
+
return ws.mission(a.mission, (state) => {
|
|
615
|
+
switch (sub) {
|
|
616
|
+
case "gap": return skills.addGap(state, a.capability, a.description);
|
|
617
|
+
case "candidate": {
|
|
618
|
+
const item = skills.addCandidate(state, {
|
|
619
|
+
name: a.name, source: a.source, purpose: a.purpose, agent: a.agent, url: a.url || "", local_path: a.local_path || "",
|
|
620
|
+
gap: a.gap || null, benefits: a.benefits || "", permissions: splitCsv(a.permissions),
|
|
621
|
+
compatibility: a.compatibility || "unknown", risks: a.risks || "", install: a.install || "",
|
|
622
|
+
});
|
|
623
|
+
audit.log(ws, state.id, "skill_candidate", { skill: item.id, name: item.name, url: item.url });
|
|
624
|
+
return item;
|
|
625
|
+
}
|
|
626
|
+
case "approve": {
|
|
627
|
+
const approval = permissions.getApproval(state, a.approval, "skill_install");
|
|
628
|
+
const item = skills.approve(state, a.skill_id, approval);
|
|
629
|
+
audit.log(ws, state.id, "skill_approved", { skill: item.id, approval: approval.id });
|
|
630
|
+
return item;
|
|
631
|
+
}
|
|
632
|
+
case "reject": return skills.reject(state, a.skill_id, a.reason);
|
|
633
|
+
case "verify": {
|
|
634
|
+
const item = skills.verify(state, a.skill_id, a.path);
|
|
635
|
+
audit.log(ws, state.id, "skill_integrated", { skill: item.id, path: item.installed_path });
|
|
636
|
+
return item;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
throw new BrainiverseError(`unknown skill command ${sub}`);
|
|
640
|
+
});
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
handlers.review = (ws, a) => ws.mission(a.mission, (state) => {
|
|
644
|
+
const review = evaluation.addReview(state, a.target, a.verdict, a.evidence || [], a.issue || [], a.reviewer || "leader");
|
|
645
|
+
audit.log(ws, state.id, "review", { target: a.target, verdict: a.verdict });
|
|
646
|
+
return review;
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
handlers.metrics = (ws, a) => evaluation.metrics(ws.readMission(a.mission));
|
|
650
|
+
|
|
651
|
+
handlers.perm = (ws, a) => ({
|
|
652
|
+
agent: a.agent, capability: a.capability, result: permissions.check(ws.readMission(a.mission), a.agent, a.capability),
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
// -- command spec -------------------------------------------------------------------
|
|
656
|
+
|
|
657
|
+
const M = { mission: { short: "m", help: "mission id (default: the active mission)" } };
|
|
658
|
+
const TEAM = { team: { help: "team id, or @mission for the active mission's team (default: the agent library)" } };
|
|
659
|
+
const MULTI = { multi: true };
|
|
660
|
+
|
|
661
|
+
const agentSub = (positional, options = {}, help = "") => ({ help, positional, options: { ...M, ...TEAM, ...options } });
|
|
662
|
+
const teamSub = (positional = [], options = {}, help = "") => ({ help, positional: ["team_ref", ...positional], options: { ...M, ...options } });
|
|
663
|
+
const taskSub = (options = {}, help = "") => ({ help, positional: ["task_id"], options: { ...M, ...options } });
|
|
664
|
+
const meetingSub = (positional = [], options = {}, help = "") => ({ help, positional: ["meeting_id", ...positional], options: { ...M, ...options } });
|
|
665
|
+
|
|
666
|
+
export const SPEC = {
|
|
667
|
+
help: "Brainiverse — a customizable multi-agent team runtime for Claude Code.",
|
|
668
|
+
subs: {
|
|
669
|
+
init: { help: "create the workspace", options: { theme: {}, terminology: { help: "themed | professional" } } },
|
|
670
|
+
config: { help: "show or set theme / terminology / host", positional: ["?key", "?value"] },
|
|
671
|
+
archetypes: { help: "list the ten archetypes" },
|
|
672
|
+
themes: { help: "list identity themes" },
|
|
673
|
+
capabilities: { help: "list capabilities, their risk, and host tools" },
|
|
674
|
+
agent: {
|
|
675
|
+
help: "create and customize agents",
|
|
676
|
+
subs: {
|
|
677
|
+
list: agentSub([], {}, "list agents (library, or --team)"),
|
|
678
|
+
new: agentSub([], { archetype: { required: true }, id: {}, name: {}, set: { ...MULTI, help: "field=value (lists: a;b;c)" } }, "create an agent"),
|
|
679
|
+
import: agentSub(["path"], {}, "import an agent JSON file (gated capabilities are stripped)"),
|
|
680
|
+
show: agentSub(["agent_id"], {}, "show a profile"),
|
|
681
|
+
edit: agentSub(["agent_id"], { set: MULTI, add: MULTI, remove: MULTI }, "edit fields"),
|
|
682
|
+
rename: agentSub(["agent_id"], { name: {}, personality: {}, style: {}, avatar: {} }, "change identity only"),
|
|
683
|
+
enable: agentSub(["agent_id"], {}, "enable an agent"),
|
|
684
|
+
disable: agentSub(["agent_id"], {}, "disable an agent"),
|
|
685
|
+
duplicate: agentSub(["agent_id", "new_id"], { name: {} }, "copy an agent"),
|
|
686
|
+
delete: agentSub(["agent_id"], {}, "delete an agent"),
|
|
687
|
+
export: agentSub(["agent_id"], { out: {} }, "export to JSON"),
|
|
688
|
+
grant: agentSub(["agent_id", "capability"], { approval: { help: "approval id (required for gated capabilities)" } }, "grant a capability"),
|
|
689
|
+
revoke: agentSub(["agent_id", "capability"], {}, "revoke a capability"),
|
|
690
|
+
test: agentSub(["agent_id"], { task: { required: true } }, "get a prompt to test the agent in isolation"),
|
|
691
|
+
},
|
|
692
|
+
},
|
|
693
|
+
team: {
|
|
694
|
+
help: "propose, build, and save teams",
|
|
695
|
+
subs: {
|
|
696
|
+
templates: { help: "list team templates" },
|
|
697
|
+
list: { help: "list saved teams" },
|
|
698
|
+
propose: { help: "propose a team for a mission", positional: ["mission_text"], options: { max: { type: "int" }, id: {} } },
|
|
699
|
+
"from-template": { help: "create a team from a template", positional: ["template"], options: { id: {} } },
|
|
700
|
+
import: { help: "import a team JSON file", positional: ["path"] },
|
|
701
|
+
show: teamSub([], {}, "show a team"),
|
|
702
|
+
approve: teamSub([], {}, "mark a draft team approved"),
|
|
703
|
+
add: teamSub([], { archetype: { default: "specialist" }, id: {}, name: {}, description: {}, expertise: { help: "comma-separated" },
|
|
704
|
+
"from-library": { help: "add a saved agent from the library" } }, "add an agent"),
|
|
705
|
+
remove: teamSub(["agent_id"], {}, "remove an agent"),
|
|
706
|
+
leader: teamSub(["agent_id"], {}, "choose the leader"),
|
|
707
|
+
rename: teamSub(["name"], {}, "rename the team"),
|
|
708
|
+
save: teamSub([], { as: {} }, "save (e.g. @mission) to the team library"),
|
|
709
|
+
export: teamSub([], { out: {} }, "export to JSON"),
|
|
710
|
+
delete: teamSub([], {}, "delete a saved team"),
|
|
711
|
+
},
|
|
712
|
+
},
|
|
713
|
+
mission: {
|
|
714
|
+
help: "start and manage missions",
|
|
715
|
+
subs: {
|
|
716
|
+
new: { help: "start a mission", options: { goal: { required: true }, team: { required: true }, id: {}, title: {},
|
|
717
|
+
success: MULTI, constraint: MULTI, assumption: MULTI, resource: MULTI, limit: { ...MULTI, help: "key=value" } } },
|
|
718
|
+
list: { help: "list missions" },
|
|
719
|
+
use: { help: "switch the active mission", positional: ["mission_id"] },
|
|
720
|
+
show: { help: "mission details", options: M },
|
|
721
|
+
pause: { help: "pause the mission", options: M },
|
|
722
|
+
resume: { help: "resume the mission", options: M },
|
|
723
|
+
close: { help: "close the mission", options: { ...M, force: { type: "bool" } } },
|
|
724
|
+
update: { help: "add criteria/constraints, change limits", options: { ...M, success: MULTI, constraint: MULTI, assumption: MULTI, resource: MULTI, limit: MULTI } },
|
|
725
|
+
finalize: { help: "save the Leader's synthesis", options: { ...M, file: { required: true } } },
|
|
726
|
+
report: { help: "render the mission debrief", options: M },
|
|
727
|
+
log: { help: "show the audit log", options: { ...M, limit: { type: "int", default: 50 } } },
|
|
728
|
+
},
|
|
729
|
+
},
|
|
730
|
+
task: {
|
|
731
|
+
help: "the task board",
|
|
732
|
+
subs: {
|
|
733
|
+
add: { help: "add a task", options: { ...M, agent: { required: true }, objective: { required: true }, input: MULTI, expect: {},
|
|
734
|
+
constraint: MULTI, after: { help: "comma-separated dependency ids" }, accept: { ...MULTI, help: "acceptance criterion" },
|
|
735
|
+
context: {}, needs: { help: "comma-separated capabilities" }, timeout: { type: "int" }, retries: { type: "int" }, parent: {} } },
|
|
736
|
+
list: { help: "list tasks", options: { ...M, status: {} } },
|
|
737
|
+
ready: { help: "tasks ready to run now", options: M },
|
|
738
|
+
show: taskSub({}, "show a task"),
|
|
739
|
+
brief: taskSub({}, "the agent's brief"),
|
|
740
|
+
start: taskSub({}, "start a task (or 'ready' for all ready tasks) and get Agent-tool calls"),
|
|
741
|
+
submit: taskSub({ file: {}, tokens: { type: "int" } }, "submit a result (JSON on stdin or --file)"),
|
|
742
|
+
accept: taskSub({ evidence: {} }, "accept submitted work"),
|
|
743
|
+
revise: taskSub({ feedback: { required: true } }, "send work back"),
|
|
744
|
+
fail: taskSub({ error: { required: true } }, "mark an attempt failed (retries within limits)"),
|
|
745
|
+
cancel: taskSub({ reason: {} }, "cancel a task"),
|
|
746
|
+
reassign: { help: "reassign a pending task", positional: ["task_id", "agent"], options: M },
|
|
747
|
+
"attach-approval": { help: "attach an approval to a task", positional: ["task_id", "approval"], options: M },
|
|
748
|
+
sweep: { help: "fail tasks that exceeded their timeout", options: M },
|
|
749
|
+
},
|
|
750
|
+
},
|
|
751
|
+
status: { help: "progress board", options: M },
|
|
752
|
+
metrics: { help: "mission metrics", options: M },
|
|
753
|
+
perm: { help: "check an agent's permission for a capability", positional: ["agent", "capability"], options: M },
|
|
754
|
+
meeting: {
|
|
755
|
+
help: "interactive meetings (war rooms)",
|
|
756
|
+
subs: {
|
|
757
|
+
start: { help: "open a meeting", options: { ...M, objective: { required: true }, with: { required: true, help: "comma-separated agent ids" } } },
|
|
758
|
+
show: meetingSub([], {}, "meeting state and transcript"),
|
|
759
|
+
ask: meetingSub([], { agent: { required: true, help: "one or more agent ids, comma-separated" }, prompt: { required: true } }, "ask agents to contribute"),
|
|
760
|
+
say: meetingSub(["text"], {}, "the Leader speaks"),
|
|
761
|
+
human: meetingSub(["text"], { answers: { help: "question id this answers" } }, "record the human's words"),
|
|
762
|
+
question: meetingSub(["text"], { to: { default: "human" } }, "record an open question"),
|
|
763
|
+
decide: meetingSub(["text"], { "human-approved": { type: "bool" } }, "record a decision"),
|
|
764
|
+
assume: meetingSub(["text"], {}, "record an assumption"),
|
|
765
|
+
action: meetingSub([], { agent: { required: true }, objective: { required: true }, accept: MULTI, after: {} }, "turn a conclusion into a task"),
|
|
766
|
+
add: meetingSub(["agent"], {}, "add a participant"),
|
|
767
|
+
remove: meetingSub(["agent"], {}, "remove a participant"),
|
|
768
|
+
pause: meetingSub([], {}, "pause"),
|
|
769
|
+
resume: meetingSub([], {}, "resume"),
|
|
770
|
+
end: meetingSub([], { summary: { required: true } }, "end with a summary"),
|
|
771
|
+
},
|
|
772
|
+
},
|
|
773
|
+
chat: {
|
|
774
|
+
help: "direct line: the human talks one-on-one with a single agent",
|
|
775
|
+
subs: {
|
|
776
|
+
start: { help: "open a direct line", positional: ["agent"], options: { ...M, topic: {} } },
|
|
777
|
+
say: { help: "send the human's message and get the Agent-tool call", positional: ["chat_id", "text"], options: M },
|
|
778
|
+
show: { help: "the conversation so far", positional: ["chat_id"], options: M },
|
|
779
|
+
list: { help: "list direct lines", options: M },
|
|
780
|
+
end: { help: "close a direct line", positional: ["chat_id"], options: { ...M, summary: {} } },
|
|
781
|
+
},
|
|
782
|
+
},
|
|
783
|
+
approval: {
|
|
784
|
+
help: "human approvals",
|
|
785
|
+
subs: {
|
|
786
|
+
request: { help: "request an approval", options: { ...M, kind: { default: "capability", help: permissions.APPROVAL_KINDS.join(" | ") },
|
|
787
|
+
detail: { required: true }, agent: {}, capability: {}, task: {}, skill: {} } },
|
|
788
|
+
resolve: { help: "record the human's decision", positional: ["approval_id", "decision"],
|
|
789
|
+
options: { ...M, note: { required: true, help: "the human's answer, quoted" } } },
|
|
790
|
+
list: { help: "list approvals", options: { ...M, status: {} } },
|
|
791
|
+
},
|
|
792
|
+
},
|
|
793
|
+
memory: {
|
|
794
|
+
help: "agent / team / project memory",
|
|
795
|
+
subs: Object.fromEntries(["add", "list", "edit", "delete"].map((name) => [name, {
|
|
796
|
+
help: `${name} memory records`,
|
|
797
|
+
positional: [...(["edit", "delete"].includes(name) ? ["record_id"] : []), ...(["add", "edit"].includes(name) ? ["text"] : [])],
|
|
798
|
+
options: { ...M, scope: { default: "project", help: "project | agent:<id> | team:<id>" }, ...(["add", "list"].includes(name) ? { tags: {} } : {}) },
|
|
799
|
+
}])),
|
|
800
|
+
},
|
|
801
|
+
skill: {
|
|
802
|
+
help: "skill discovery and integration",
|
|
803
|
+
subs: {
|
|
804
|
+
local: { help: "skills installed locally", positional: ["?query"] },
|
|
805
|
+
scan: { help: "static security scan of a skill directory", positional: ["path"], options: { ...M, candidate: {} } },
|
|
806
|
+
list: { help: "gaps and candidates", options: M },
|
|
807
|
+
recommend: { help: "the top candidates (max 3)", options: M },
|
|
808
|
+
gap: { help: "record a capability gap", positional: ["capability", "description"], options: M },
|
|
809
|
+
candidate: { help: "record a skill that was actually found", options: { ...M, name: { required: true },
|
|
810
|
+
source: { required: true, help: "skills.sh, github, local, ..." }, purpose: { required: true }, agent: { required: true },
|
|
811
|
+
url: {}, "local-path": {}, gap: {}, benefits: {}, permissions: {}, compatibility: {}, risks: {},
|
|
812
|
+
install: { help: "the exact install command, as documented by the source" } } },
|
|
813
|
+
approve: { help: "approve installation (needs a skill_install approval)", positional: ["skill_id"], options: { ...M, approval: { required: true } } },
|
|
814
|
+
reject: { help: "reject a candidate", positional: ["skill_id"], options: { ...M, reason: { required: true } } },
|
|
815
|
+
verify: { help: "verify an install and attach it to its agent", positional: ["skill_id"], options: { ...M, path: { required: true } } },
|
|
816
|
+
},
|
|
817
|
+
},
|
|
818
|
+
review: { help: "record an evidence-backed review", positional: ["target"], options: { ...M,
|
|
819
|
+
verdict: { required: true, help: evaluation.VERDICTS.join(" | ") }, evidence: MULTI, issue: MULTI, reviewer: {} } },
|
|
820
|
+
},
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
function print(data) {
|
|
824
|
+
process.stdout.write((typeof data === "string" ? data : JSON.stringify(data, null, 2)) + "\n");
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
export function main(argv = process.argv.slice(2)) {
|
|
828
|
+
let home;
|
|
829
|
+
const rest = [];
|
|
830
|
+
for (let i = 0; i < argv.length; i++) {
|
|
831
|
+
if (argv[i] === "--home") home = argv[++i];
|
|
832
|
+
else if (argv[i].startsWith("--home=")) home = argv[i].slice(7);
|
|
833
|
+
else rest.push(argv[i]);
|
|
834
|
+
}
|
|
835
|
+
try {
|
|
836
|
+
const args = parse(rest, SPEC);
|
|
837
|
+
const [command, sub] = args._path;
|
|
838
|
+
if (permissions.actingAgent() && !AGENT_ALLOWED.has([command, sub].filter(Boolean).join(" "))) {
|
|
839
|
+
permissions.requireLeader([command, sub].filter(Boolean).join(" "));
|
|
840
|
+
}
|
|
841
|
+
if (args.verdict && !evaluation.VERDICTS.includes(args.verdict)) {
|
|
842
|
+
throw new BrainiverseError(`verdict must be one of ${evaluation.VERDICTS.join(", ")}`);
|
|
843
|
+
}
|
|
844
|
+
const ws = new Workspace(home);
|
|
845
|
+
if (command !== "init") ws.require();
|
|
846
|
+
print(handlers[command](ws, args, sub));
|
|
847
|
+
return 0;
|
|
848
|
+
} catch (err) {
|
|
849
|
+
if (err instanceof HelpRequested) {
|
|
850
|
+
print(err.message);
|
|
851
|
+
return 0;
|
|
852
|
+
}
|
|
853
|
+
if (err instanceof BrainiverseError || err.code === "ENOENT") {
|
|
854
|
+
process.stderr.write(`error: ${err.message}\n`);
|
|
855
|
+
return 1;
|
|
856
|
+
}
|
|
857
|
+
throw err;
|
|
858
|
+
}
|
|
859
|
+
}
|