brainclaw 1.5.5 → 1.7.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/README.md +5 -4
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +124 -7
- package/dist/commands/bootstrap-loop.js +206 -0
- package/dist/commands/loop.js +156 -0
- package/dist/commands/loops-handlers.js +110 -55
- package/dist/commands/mcp-read-handlers.js +37 -0
- package/dist/commands/mcp-schemas.generated.js +33 -0
- package/dist/commands/mcp.js +661 -207
- package/dist/commands/questions.js +180 -0
- package/dist/commands/reply.js +190 -0
- package/dist/commands/session-end.js +105 -3
- package/dist/commands/session-start.js +32 -53
- package/dist/commands/setup.js +64 -13
- package/dist/commands/switch.js +17 -1
- package/dist/core/agent-capability.js +19 -0
- package/dist/core/agent-files.js +86 -0
- package/dist/core/agent-integrations.js +34 -0
- package/dist/core/agent-inventory.js +27 -0
- package/dist/core/agentrun-reconciler.js +130 -0
- package/dist/core/ai-agent-detection.js +17 -1
- package/dist/core/claims.js +54 -3
- package/dist/core/dirty-scope.js +242 -0
- package/dist/core/dispatch-status.js +219 -0
- package/dist/core/entity-operations.js +128 -9
- package/dist/core/execution-adapters.js +38 -2
- package/dist/core/facade-schema.js +71 -0
- package/dist/core/federation-cloud.js +27 -12
- package/dist/core/federation-materialize.js +57 -0
- package/dist/core/instruction-templates.js +2 -0
- package/dist/core/loops/bootstrap-acquire.js +195 -0
- package/dist/core/loops/facade-schema.js +68 -1
- package/dist/core/loops/hooks/bootstrap-write.js +144 -0
- package/dist/core/loops/hooks/notify-operator.js +148 -0
- package/dist/core/loops/hooks/survey-source-reader.js +256 -0
- package/dist/core/loops/index.js +8 -2
- package/dist/core/loops/next-expected.js +63 -0
- package/dist/core/loops/presets/bootstrap.js +75 -0
- package/dist/core/loops/presets/index.js +16 -0
- package/dist/core/loops/store.js +224 -4
- package/dist/core/loops/types.js +346 -1
- package/dist/core/loops/verbs.js +739 -6
- package/dist/core/schema.js +30 -2
- package/dist/core/state.js +62 -0
- package/dist/core/worktree.js +58 -0
- package/dist/facts.js +360 -5
- package/dist/facts.json +359 -4
- package/docs/cli.md +10 -7
- package/docs/concepts/dispatch-lifecycle.md +228 -0
- package/docs/concepts/loop-engine.md +55 -0
- package/docs/concepts/multi-agent-workflows.md +167 -166
- package/docs/concepts/troubleshooting.md +36 -2
- package/docs/index.md +4 -3
- package/docs/integrations/hermes.md +78 -0
- package/docs/integrations/overview.md +22 -18
- package/docs/mcp-schema-changelog.md +8 -1
- package/docs/quickstart-existing-project.md +1 -1
- package/package.json +5 -4
package/dist/commands/setup.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import readline from 'node:readline/promises';
|
|
4
5
|
import { spawnSync } from 'node:child_process';
|
|
@@ -7,7 +8,7 @@ import { detectAiAgent } from '../core/ai-agent-detection.js';
|
|
|
7
8
|
import { buildAiSurfaceInventory, renderAiSurfaceUsageHints } from '../core/ai-surface-inventory.js';
|
|
8
9
|
import { buildMachineProfile, saveMachineProfile, loadMachineProfile } from '../core/machine-profile.js';
|
|
9
10
|
import { buildAgentInventory, saveAgentInventory, loadAgentInventory } from '../core/agent-inventory.js';
|
|
10
|
-
import { ensureClaudeCodeUserSettings, ensureClaudeCodeUserCommand, ensureCursorMcpConfig, ensureWindsurfMcpConfig, ensureAntigravityMcpConfig, ensureContinueUserMcpConfig, ensureContinueUserPermissions, ensureCodexMcpConfig, writeDetectedAgentAutoConfig, describeAutoConfigWrite, ensureGitignoreEntries, collectWorkspaceGitignoreEntries, BRAINCLAW_EXCLUSIVE_DIRECTORIES, } from '../core/agent-files.js';
|
|
11
|
+
import { ensureClaudeCodeUserSettings, ensureClaudeCodeUserCommand, ensureCursorMcpConfig, ensureWindsurfMcpConfig, ensureAntigravityMcpConfig, ensureContinueUserMcpConfig, ensureContinueUserPermissions, ensureCodexMcpConfig, ensureHermesMcpConfig, writeDetectedAgentAutoConfig, describeAutoConfigWrite, ensureGitignoreEntries, collectWorkspaceGitignoreEntries, BRAINCLAW_EXCLUSIVE_DIRECTORIES, } from '../core/agent-files.js';
|
|
11
12
|
import { MEMORY_DIR, memoryExists, ensureMemoryDir } from '../core/io.js';
|
|
12
13
|
import { saveConfig, defaultConfig } from '../core/config.js';
|
|
13
14
|
import { readSetupState, resolveHomeDir, writeSetupState } from '../core/setup-state.js';
|
|
@@ -25,6 +26,9 @@ export const ALL_KNOWN_AGENTS = [
|
|
|
25
26
|
'antigravity',
|
|
26
27
|
'continue',
|
|
27
28
|
'roo',
|
|
29
|
+
'kilocode',
|
|
30
|
+
'mistral-vibe',
|
|
31
|
+
'hermes',
|
|
28
32
|
'openclaw',
|
|
29
33
|
'nanoclaw',
|
|
30
34
|
'nemoclaw',
|
|
@@ -135,13 +139,46 @@ export function parseRepoSelection(choice, repos, cwd = process.cwd()) {
|
|
|
135
139
|
return indices.map((i) => repos[i]);
|
|
136
140
|
}
|
|
137
141
|
// ─── Step 4: Agent selection ──────────────────────────────────────────────────
|
|
138
|
-
|
|
142
|
+
function uniqueKnownAgents(names) {
|
|
143
|
+
const seen = new Set();
|
|
144
|
+
const result = [];
|
|
145
|
+
for (const name of names) {
|
|
146
|
+
if (!name || !ALL_KNOWN_AGENTS.includes(name))
|
|
147
|
+
continue;
|
|
148
|
+
if (seen.has(name))
|
|
149
|
+
continue;
|
|
150
|
+
seen.add(name);
|
|
151
|
+
result.push(name);
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
export function getInstalledAgentNames(inventory) {
|
|
156
|
+
return uniqueKnownAgents(inventory?.agents.filter((agent) => agent.installed).map((agent) => agent.name) ?? []);
|
|
157
|
+
}
|
|
158
|
+
export function getDetectedSetupAgentNames(detected, installedAgents = []) {
|
|
159
|
+
return uniqueKnownAgents([
|
|
160
|
+
detected,
|
|
161
|
+
...ALL_KNOWN_AGENTS.filter((agent) => installedAgents.includes(agent)),
|
|
162
|
+
]);
|
|
163
|
+
}
|
|
164
|
+
export function parseAgentSelection(choice, detected, installedAgents = []) {
|
|
139
165
|
const c = choice.trim().toLowerCase();
|
|
140
166
|
if (c === 'a' || c === 'all')
|
|
141
167
|
return [...ALL_KNOWN_AGENTS];
|
|
142
|
-
if (c === 'd' || c === 'detected')
|
|
143
|
-
return detected
|
|
144
|
-
|
|
168
|
+
if (c === 'd' || c === 'detected' || c === 'installed') {
|
|
169
|
+
return getDetectedSetupAgentNames(detected, installedAgents);
|
|
170
|
+
}
|
|
171
|
+
const selected = [];
|
|
172
|
+
for (const token of c.split(',').map((a) => a.trim()).filter(Boolean)) {
|
|
173
|
+
const index = Number.parseInt(token, 10);
|
|
174
|
+
if (/^\d+$/.test(token) && index >= 1 && index <= ALL_KNOWN_AGENTS.length) {
|
|
175
|
+
selected.push(ALL_KNOWN_AGENTS[index - 1]);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (ALL_KNOWN_AGENTS.includes(token))
|
|
179
|
+
selected.push(token);
|
|
180
|
+
}
|
|
181
|
+
return uniqueKnownAgents(selected);
|
|
145
182
|
}
|
|
146
183
|
// ─── Step 5: Global install ───────────────────────────────────────────────────
|
|
147
184
|
export function initUserStore(home, env = process.env) {
|
|
@@ -237,6 +274,11 @@ export function runGlobalInstall(selectedAgents, env = process.env) {
|
|
|
237
274
|
if (r && (r.created || r.updated))
|
|
238
275
|
written.push(r.filePath);
|
|
239
276
|
}
|
|
277
|
+
if (selectedAgents.includes('hermes')) {
|
|
278
|
+
const r = ensureHermesMcpConfig(home);
|
|
279
|
+
if (r && (r.created || r.updated))
|
|
280
|
+
written.push(r.filePath);
|
|
281
|
+
}
|
|
240
282
|
return written;
|
|
241
283
|
}
|
|
242
284
|
// ─── Step 6: Init repos + configure agents ────────────────────────────────────
|
|
@@ -331,30 +373,35 @@ function logDetectedAgentSurfaces(detectedName, detectedSurfaces) {
|
|
|
331
373
|
console.log('These surfaces are tracked separately from coding agents and will use tailored onboarding flows.');
|
|
332
374
|
}
|
|
333
375
|
}
|
|
334
|
-
async function resolveSelectedAgentsForSetup(options, detectedName) {
|
|
376
|
+
async function resolveSelectedAgentsForSetup(options, detectedName, installedAgents = []) {
|
|
377
|
+
const detectedSetupAgents = getDetectedSetupAgentNames(detectedName, installedAgents);
|
|
335
378
|
console.log('Supported agents:');
|
|
336
379
|
ALL_KNOWN_AGENTS.forEach((a, i) => {
|
|
337
380
|
const tag = a === detectedName ? ' ← detected' : '';
|
|
338
|
-
|
|
381
|
+
const installedTag = installedAgents.includes(a) ? ' ← installed' : '';
|
|
382
|
+
console.log(` ${i + 1}) ${a}${tag}${tag ? '' : installedTag}`);
|
|
339
383
|
});
|
|
384
|
+
if (detectedSetupAgents.length > 0) {
|
|
385
|
+
console.log(`Detected install set: ${detectedSetupAgents.join(', ')}`);
|
|
386
|
+
}
|
|
340
387
|
let agentChoice;
|
|
341
388
|
if (options.agents) {
|
|
342
389
|
agentChoice = options.agents;
|
|
343
390
|
}
|
|
344
391
|
else if (options.yes || !process.stdin.isTTY) {
|
|
345
|
-
agentChoice =
|
|
392
|
+
agentChoice = detectedSetupAgents.length > 0 ? 'detected' : 'all';
|
|
346
393
|
}
|
|
347
394
|
else {
|
|
348
|
-
const defaultChoice =
|
|
395
|
+
const defaultChoice = detectedSetupAgents.length > 0 ? 'detected' : 'all';
|
|
349
396
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
350
397
|
try {
|
|
351
|
-
agentChoice = (await rl.question(`Configure agents: (d)etected, (a)ll, or numbers e.g. 1,3 [${defaultChoice}]: `)).trim() || defaultChoice;
|
|
398
|
+
agentChoice = (await rl.question(`Configure agents: (d)etected installed, (a)ll, names, or numbers e.g. 1,3 [${defaultChoice}]: `)).trim() || defaultChoice;
|
|
352
399
|
}
|
|
353
400
|
finally {
|
|
354
401
|
rl.close();
|
|
355
402
|
}
|
|
356
403
|
}
|
|
357
|
-
const selectedAgents = parseAgentSelection(agentChoice, detectedName);
|
|
404
|
+
const selectedAgents = parseAgentSelection(agentChoice, detectedName, installedAgents);
|
|
358
405
|
console.log(`Selected agents: ${selectedAgents.length === 0 ? '(none)' : selectedAgents.join(', ')}`);
|
|
359
406
|
return selectedAgents;
|
|
360
407
|
}
|
|
@@ -364,10 +411,12 @@ export async function runSetupMachine(options = {}) {
|
|
|
364
411
|
const detectedName = detectedAi?.name;
|
|
365
412
|
const testMode = process.env.BRAINCLAW_TEST_MODE === '1';
|
|
366
413
|
const detectedSurfaces = testMode ? [] : buildAiSurfaceInventory();
|
|
414
|
+
const agentInventory = testMode ? undefined : buildAgentInventory(resolveHomeDir(env) ?? os.homedir(), env);
|
|
415
|
+
const installedAgents = getInstalledAgentNames(agentInventory);
|
|
367
416
|
console.log(BRAINCLAW_ASCII);
|
|
368
417
|
console.log('Machine bootstrap only — no repositories will be scanned or initialized.');
|
|
369
418
|
logDetectedAgentSurfaces(detectedName, detectedSurfaces);
|
|
370
|
-
const selectedAgents = await resolveSelectedAgentsForSetup(options, detectedName);
|
|
419
|
+
const selectedAgents = await resolveSelectedAgentsForSetup(options, detectedName, installedAgents);
|
|
371
420
|
console.log('\n→ Installing machine-level brainclaw prerequisites...');
|
|
372
421
|
const written = runGlobalInstall(selectedAgents, env);
|
|
373
422
|
if (written.length > 0) {
|
|
@@ -484,8 +533,10 @@ export async function runSetup(options = {}) {
|
|
|
484
533
|
const detectedName = detectedAi?.name;
|
|
485
534
|
const testMode = process.env.BRAINCLAW_TEST_MODE === '1';
|
|
486
535
|
const detectedSurfaces = testMode ? [] : buildAiSurfaceInventory();
|
|
536
|
+
const agentInventory = testMode ? undefined : buildAgentInventory(resolveHomeDir(env) ?? os.homedir(), env);
|
|
537
|
+
const installedAgents = getInstalledAgentNames(agentInventory);
|
|
487
538
|
logDetectedAgentSurfaces(detectedName, detectedSurfaces);
|
|
488
|
-
const selectedAgents = await resolveSelectedAgentsForSetup(options, detectedName);
|
|
539
|
+
const selectedAgents = await resolveSelectedAgentsForSetup(options, detectedName, installedAgents);
|
|
489
540
|
// Step 5: Global install
|
|
490
541
|
console.log('\n→ Installing global brainclaw prerequisites...');
|
|
491
542
|
const written = runGlobalInstall(selectedAgents, env);
|
package/dist/commands/switch.js
CHANGED
|
@@ -3,6 +3,7 @@ import { loadActiveProject, saveActiveProject, clearActiveProject } from '../cor
|
|
|
3
3
|
import { loadCurrentSession, saveCurrentSession } from '../core/identity.js';
|
|
4
4
|
import { memoryExists } from '../core/io.js';
|
|
5
5
|
import { resolveProjectRef } from '../core/store-resolution.js';
|
|
6
|
+
import { resolveProjectCwd } from '../core/cross-project.js';
|
|
6
7
|
import { scanNestedBrainclawProjects } from '../core/workspace-projects.js';
|
|
7
8
|
import { loadConfig } from '../core/config.js';
|
|
8
9
|
/**
|
|
@@ -16,7 +17,22 @@ export function switchProject(projectRef, options = {}) {
|
|
|
16
17
|
if (!wsRoot) {
|
|
17
18
|
throw new Error('No brainclaw workspace found. Run `brainclaw init` first.');
|
|
18
19
|
}
|
|
19
|
-
|
|
20
|
+
// pln#515 step 4 — resolution priority:
|
|
21
|
+
// 1. resolveProjectRef: workspace store-chain children (existing path)
|
|
22
|
+
// 2. resolveProjectCwd: cross_project_links (added so bclaw_switch can
|
|
23
|
+
// target externally-linked projects, not just store-chain children).
|
|
24
|
+
// resolveProjectCwd returns the original cwd on no-match, so we check
|
|
25
|
+
// for a real change before treating it as a resolution.
|
|
26
|
+
let resolved = resolveProjectRef(projectRef, cwd);
|
|
27
|
+
if (!resolved) {
|
|
28
|
+
try {
|
|
29
|
+
const linkResolved = resolveProjectCwd(projectRef, cwd);
|
|
30
|
+
if (linkResolved !== cwd) {
|
|
31
|
+
resolved = linkResolved;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch { /* link resolution failure surfaces as the same error below */ }
|
|
35
|
+
}
|
|
20
36
|
if (!resolved) {
|
|
21
37
|
throw new Error(`Cannot resolve project "${projectRef}". Use bclaw_switch with list=true to see available projects.`);
|
|
22
38
|
}
|
|
@@ -23,6 +23,7 @@ const AGENT_ALIASES = {
|
|
|
23
23
|
'gemini': 'antigravity',
|
|
24
24
|
'mistral': 'mistral-vibe',
|
|
25
25
|
'vibe': 'mistral-vibe',
|
|
26
|
+
'hermes-agent': 'hermes',
|
|
26
27
|
};
|
|
27
28
|
/** Resolve an alias to its canonical agent name, or return the input unchanged. */
|
|
28
29
|
export function resolveAgentAlias(name) {
|
|
@@ -227,6 +228,24 @@ const PROFILES = {
|
|
|
227
228
|
invoke_review_template: 'vibe --prompt "{prompt}" --auto-approve --max-turns 5',
|
|
228
229
|
invoke_consult_template: 'vibe --prompt "{prompt}" --auto-approve --max-turns 3',
|
|
229
230
|
},
|
|
231
|
+
// Hermes Agent (Nous Research) — autonomous, skills-first agent with native
|
|
232
|
+
// MCP client support via ~/.hermes/config.yaml. Brainclaw uses Hermes as a
|
|
233
|
+
// Tier B surface for now: MCP + universal .agents/skills/ skill, no native
|
|
234
|
+
// Brainclaw hooks until a dedicated Hermes plugin is shipped and validated.
|
|
235
|
+
hermes: {
|
|
236
|
+
name: 'hermes', category: 'autonomous-agent', workflowModel: 'task-based',
|
|
237
|
+
hasMcp: true, hasHooks: false, hasAutoApprove: false, hasSkills: true, hasRules: false,
|
|
238
|
+
instructionFile: 'AGENTS.md', sharedInstructionFile: true, mcpConfigScope: 'machine', templateTier: 'B',
|
|
239
|
+
role_capabilities: ['execute', 'review', 'consult'],
|
|
240
|
+
runtime: { mcp_direct: true, hooks: false, canBeSpawnedCli: true, canSpawnOtherCli: false, inbox: false },
|
|
241
|
+
max_concurrent_tasks: 1,
|
|
242
|
+
prompt_delivery: { methods: ['inline_arg', 'temp_file'], preferred: 'inline_arg', max_inline_length: 8000 },
|
|
243
|
+
execution_env: { surface: 'cli' },
|
|
244
|
+
invoke_template: 'hermes chat -q "{prompt}"',
|
|
245
|
+
invoke_binary: 'hermes',
|
|
246
|
+
invoke_review_template: 'hermes chat -q "{prompt}"',
|
|
247
|
+
invoke_consult_template: 'hermes chat -q "{prompt}"',
|
|
248
|
+
},
|
|
230
249
|
// --- Autonomous agents (headless, task-based or scheduled) ---
|
|
231
250
|
openclaw: {
|
|
232
251
|
name: 'openclaw', category: 'autonomous-agent', workflowModel: 'task-based',
|
package/dist/core/agent-files.js
CHANGED
|
@@ -338,6 +338,7 @@ export const AGENT_EXPORT_REGISTRY = [
|
|
|
338
338
|
{ agentName: 'roo', format: 'roo', relativePath: '.roo/rules/brainclaw.md' },
|
|
339
339
|
{ agentName: 'kilocode', format: 'kilocode', relativePath: '.kilo/rules/brainclaw.md' },
|
|
340
340
|
{ agentName: 'mistral-vibe', format: 'agents-md', relativePath: 'AGENTS.md' },
|
|
341
|
+
{ agentName: 'hermes', format: 'agents-md', relativePath: 'AGENTS.md' },
|
|
341
342
|
{ agentName: 'opencode', format: 'agents-md', relativePath: 'AGENTS.md' },
|
|
342
343
|
{ agentName: 'antigravity', format: 'gemini-md', relativePath: 'GEMINI.md' },
|
|
343
344
|
{ agentName: 'brainclaw', format: 'board-md', relativePath: 'BOARD.md' },
|
|
@@ -456,6 +457,8 @@ const ROO_MCP_RELATIVE_PATH = '.roo/mcp.json';
|
|
|
456
457
|
const KILOCODE_MCP_RELATIVE_PATH = '.kilo/mcp.json';
|
|
457
458
|
const KILOCODE_CONFIG_RELATIVE_PATH = 'kilo.jsonc';
|
|
458
459
|
const MISTRAL_VIBE_CONFIG_RELATIVE_PATH = '.vibe/config.toml';
|
|
460
|
+
const HERMES_CONFIG_RELATIVE_PATH = '.hermes/config.yaml';
|
|
461
|
+
const HERMES_EXTERNAL_SKILLS_RELATIVE_PATH = '.agents/skills';
|
|
459
462
|
const CONTINUE_CONFIG_RELATIVE_PATH = '.continue/config.json';
|
|
460
463
|
const CONTINUE_PERMISSIONS_RELATIVE_PATH = '.continue/permissions.yaml';
|
|
461
464
|
const OPENCODE_CONFIG_RELATIVE_PATH = 'opencode.json';
|
|
@@ -493,6 +496,7 @@ export const LOCAL_ONLY_AGENT_WORKSPACE_FILES = [
|
|
|
493
496
|
KILOCODE_MCP_RELATIVE_PATH,
|
|
494
497
|
KILOCODE_CONFIG_RELATIVE_PATH,
|
|
495
498
|
MISTRAL_VIBE_CONFIG_RELATIVE_PATH,
|
|
499
|
+
HERMES_CONFIG_RELATIVE_PATH,
|
|
496
500
|
CONTINUE_CONFIG_RELATIVE_PATH,
|
|
497
501
|
OPENCODE_CONFIG_RELATIVE_PATH,
|
|
498
502
|
WINDSURF_MCP_RELATIVE_PATH,
|
|
@@ -1278,6 +1282,78 @@ export function ensureMistralVibeMcpConfig(cwd) {
|
|
|
1278
1282
|
relativePath: MISTRAL_VIBE_CONFIG_RELATIVE_PATH,
|
|
1279
1283
|
};
|
|
1280
1284
|
}
|
|
1285
|
+
const HERMES_BRAINCLAW_MCP_TOOLS = [
|
|
1286
|
+
'bclaw_work',
|
|
1287
|
+
'bclaw_context',
|
|
1288
|
+
'bclaw_find',
|
|
1289
|
+
'bclaw_get',
|
|
1290
|
+
'bclaw_create',
|
|
1291
|
+
'bclaw_update',
|
|
1292
|
+
'bclaw_transition',
|
|
1293
|
+
];
|
|
1294
|
+
export function ensureHermesMcpConfig(homeDir, workspacePath) {
|
|
1295
|
+
if (!homeDir)
|
|
1296
|
+
return undefined;
|
|
1297
|
+
const filePath = path.join(homeDir, HERMES_CONFIG_RELATIVE_PATH);
|
|
1298
|
+
let existing = {};
|
|
1299
|
+
let existed = false;
|
|
1300
|
+
if (fs.existsSync(filePath)) {
|
|
1301
|
+
existed = true;
|
|
1302
|
+
try {
|
|
1303
|
+
const parsed = yaml.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
1304
|
+
existing = isJsonObject(parsed) ? { ...parsed } : {};
|
|
1305
|
+
}
|
|
1306
|
+
catch {
|
|
1307
|
+
existing = {};
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
const mcpServers = isJsonObject(existing.mcp_servers) ? { ...existing.mcp_servers } : {};
|
|
1311
|
+
const current = isJsonObject(mcpServers.brainclaw) ? { ...mcpServers.brainclaw } : {};
|
|
1312
|
+
const currentEnv = isJsonObject(current.env) ? { ...current.env } : {};
|
|
1313
|
+
const currentTools = isJsonObject(current.tools) ? { ...current.tools } : {};
|
|
1314
|
+
const skills = isJsonObject(existing.skills) ? { ...existing.skills } : {};
|
|
1315
|
+
const externalDirs = Array.isArray(skills.external_dirs)
|
|
1316
|
+
? skills.external_dirs.filter((value) => typeof value === 'string')
|
|
1317
|
+
: [];
|
|
1318
|
+
if (workspacePath) {
|
|
1319
|
+
const projectSkillsDir = path.resolve(workspacePath, HERMES_EXTERNAL_SKILLS_RELATIVE_PATH);
|
|
1320
|
+
const normalized = projectSkillsDir.replace(/\\/g, '/').toLowerCase();
|
|
1321
|
+
if (!externalDirs.some((dir) => dir.replace(/\\/g, '/').toLowerCase() === normalized)) {
|
|
1322
|
+
externalDirs.push(projectSkillsDir);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
const mcpCmd = getBrainclawMcpCommand();
|
|
1326
|
+
mcpServers.brainclaw = {
|
|
1327
|
+
...current,
|
|
1328
|
+
command: typeof current.command === 'string' ? current.command : mcpCmd.command,
|
|
1329
|
+
args: Array.isArray(current.args) ? current.args : mcpCmd.args,
|
|
1330
|
+
env: {
|
|
1331
|
+
...currentEnv,
|
|
1332
|
+
BRAINCLAW_AGENT: 'hermes',
|
|
1333
|
+
},
|
|
1334
|
+
tools: {
|
|
1335
|
+
...currentTools,
|
|
1336
|
+
include: Array.isArray(currentTools.include) ? currentTools.include : HERMES_BRAINCLAW_MCP_TOOLS,
|
|
1337
|
+
prompts: typeof currentTools.prompts === 'boolean' ? currentTools.prompts : false,
|
|
1338
|
+
resources: typeof currentTools.resources === 'boolean' ? currentTools.resources : false,
|
|
1339
|
+
},
|
|
1340
|
+
};
|
|
1341
|
+
const nextConfig = {
|
|
1342
|
+
...existing,
|
|
1343
|
+
mcp_servers: mcpServers,
|
|
1344
|
+
...(externalDirs.length > 0 ? { skills: { ...skills, external_dirs: externalDirs } } : {}),
|
|
1345
|
+
};
|
|
1346
|
+
const content = `# Managed by brainclaw — preserves existing Hermes settings\n${yaml.stringify(nextConfig)}`;
|
|
1347
|
+
const { created, updated } = writeTextFileIfChanged(filePath, content);
|
|
1348
|
+
return {
|
|
1349
|
+
kind: 'mcp',
|
|
1350
|
+
label: 'Hermes MCP settings',
|
|
1351
|
+
created: !existed && created,
|
|
1352
|
+
updated: existed && updated,
|
|
1353
|
+
filePath,
|
|
1354
|
+
relativePath: HERMES_CONFIG_RELATIVE_PATH,
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1281
1357
|
export function ensureCodexMcpConfig(homeDir, env = process.env) {
|
|
1282
1358
|
const codexHome = env.CODEX_HOME?.trim() || (homeDir ? path.join(homeDir, '.codex') : null);
|
|
1283
1359
|
if (!codexHome)
|
|
@@ -1814,6 +1890,13 @@ export function writeDetectedAgentAutoConfig(agentName, cwd, env = process.env)
|
|
|
1814
1890
|
return [ensureKilocodeMcpConfig(cwd), ensureKilocodeConfig(cwd), ensureUniversalBrainclawSkill(cwd)];
|
|
1815
1891
|
case 'mistral-vibe':
|
|
1816
1892
|
return [ensureMistralVibeMcpConfig(cwd), ensureUniversalBrainclawSkill(cwd)];
|
|
1893
|
+
case 'hermes': {
|
|
1894
|
+
const results = [ensureUniversalBrainclawSkill(cwd)];
|
|
1895
|
+
const mcp = ensureHermesMcpConfig(resolveHomeDir(env), cwd);
|
|
1896
|
+
if (mcp)
|
|
1897
|
+
results.push(mcp);
|
|
1898
|
+
return results;
|
|
1899
|
+
}
|
|
1817
1900
|
case 'codex': {
|
|
1818
1901
|
const results = [ensureUniversalBrainclawSkill(cwd)];
|
|
1819
1902
|
const result = ensureCodexMcpConfig(resolveHomeDir(env), env);
|
|
@@ -1916,6 +1999,8 @@ export function writeExportCompanionFiles(format, cwd, env = process.env) {
|
|
|
1916
1999
|
results.push(hooks);
|
|
1917
2000
|
return results;
|
|
1918
2001
|
}
|
|
2002
|
+
case 'agents-md':
|
|
2003
|
+
return [ensureUniversalBrainclawSkill(cwd)];
|
|
1919
2004
|
default:
|
|
1920
2005
|
return [];
|
|
1921
2006
|
}
|
|
@@ -1956,6 +2041,7 @@ export function patchAllMcpConfigs(cwd, env = process.env) {
|
|
|
1956
2041
|
ensureAntigravityMcpConfig(homeDir),
|
|
1957
2042
|
ensureOpenClawMcpConfig(homeDir),
|
|
1958
2043
|
ensureCodexMcpConfig(homeDir, env),
|
|
2044
|
+
ensureHermesMcpConfig(homeDir),
|
|
1959
2045
|
];
|
|
1960
2046
|
for (const r of userConfigs) {
|
|
1961
2047
|
if (r)
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import yaml from 'yaml';
|
|
5
6
|
import { getInstalledBrainclawVersion } from './brainclaw-version.js';
|
|
6
7
|
import { getAgentCapabilityProfile } from './agent-capability.js';
|
|
7
8
|
const SUPPORTED_AGENT_INTEGRATION_NAMES = new Set([
|
|
@@ -16,12 +17,17 @@ const SUPPORTED_AGENT_INTEGRATION_NAMES = new Set([
|
|
|
16
17
|
'continue',
|
|
17
18
|
'roo',
|
|
18
19
|
'kilocode',
|
|
20
|
+
'mistral-vibe',
|
|
21
|
+
'hermes',
|
|
19
22
|
'openclaw',
|
|
20
23
|
'nanoclaw',
|
|
21
24
|
'nemoclaw',
|
|
22
25
|
'picoclaw',
|
|
23
26
|
'zeroclaw',
|
|
24
27
|
]);
|
|
28
|
+
function isRecord(value) {
|
|
29
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
30
|
+
}
|
|
25
31
|
const DEFAULT_SURFACES = {
|
|
26
32
|
'github-copilot': [
|
|
27
33
|
{ kind: 'instructions', location: 'workspace', path: '.github/copilot-instructions.md' },
|
|
@@ -85,6 +91,16 @@ const DEFAULT_SURFACES = {
|
|
|
85
91
|
{ kind: 'mcp', location: 'workspace', path: '.kilo/mcp.json' },
|
|
86
92
|
{ kind: 'skill', location: 'workspace', path: '.agents/skills/brainclaw/SKILL.md' },
|
|
87
93
|
],
|
|
94
|
+
'mistral-vibe': [
|
|
95
|
+
{ kind: 'instructions', location: 'workspace', path: 'AGENTS.md' },
|
|
96
|
+
{ kind: 'mcp', location: 'workspace', path: '.vibe/config.toml' },
|
|
97
|
+
{ kind: 'skill', location: 'workspace', path: '.agents/skills/brainclaw/SKILL.md' },
|
|
98
|
+
],
|
|
99
|
+
'hermes': [
|
|
100
|
+
{ kind: 'instructions', location: 'workspace', path: 'AGENTS.md' },
|
|
101
|
+
{ kind: 'mcp', location: 'machine', path: '.hermes/config.yaml' },
|
|
102
|
+
{ kind: 'skill', location: 'workspace', path: '.agents/skills/brainclaw/SKILL.md' },
|
|
103
|
+
],
|
|
88
104
|
'openclaw': [
|
|
89
105
|
{ kind: 'skill', location: 'machine', path: '.openclaw/workspace/skills/brainclaw/SKILL.md' },
|
|
90
106
|
{ kind: 'mcp', location: 'machine', path: '.openclaw/mcp.json' },
|
|
@@ -161,6 +177,24 @@ export function extractMcpCommandVal(agentName, expectedPath) {
|
|
|
161
177
|
is_valid: true,
|
|
162
178
|
};
|
|
163
179
|
}
|
|
180
|
+
if (expectedPath.endsWith('.yaml') || expectedPath.endsWith('.yml')) {
|
|
181
|
+
try {
|
|
182
|
+
const parsed = yaml.parse(content);
|
|
183
|
+
const root = isRecord(parsed) ? parsed : {};
|
|
184
|
+
const servers = isRecord(root.mcp_servers) ? root.mcp_servers : {};
|
|
185
|
+
const bc = isRecord(servers.brainclaw) ? servers.brainclaw : {};
|
|
186
|
+
const cmd = bc.command;
|
|
187
|
+
const args = bc.args;
|
|
188
|
+
return {
|
|
189
|
+
command: typeof cmd === 'string' ? cmd : undefined,
|
|
190
|
+
args: Array.isArray(args) ? args.filter((a) => typeof a === 'string') : undefined,
|
|
191
|
+
is_valid: true,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return { is_valid: false };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
164
198
|
try {
|
|
165
199
|
const j = JSON.parse(content);
|
|
166
200
|
let cmd;
|
|
@@ -300,6 +300,33 @@ const AGENT_DEFINITIONS = [
|
|
|
300
300
|
hooks_support: false,
|
|
301
301
|
instruction_file: '.continue/rules/',
|
|
302
302
|
},
|
|
303
|
+
{
|
|
304
|
+
name: 'hermes',
|
|
305
|
+
detect: (home, env) => {
|
|
306
|
+
if (env.HERMES_SESSION_ID || env.HERMES_AGENT || env.HERMES_HOME) {
|
|
307
|
+
return { installed: true, method: 'HERMES_* env' };
|
|
308
|
+
}
|
|
309
|
+
if (fs.existsSync(path.join(home, '.hermes'))) {
|
|
310
|
+
return { installed: true, method: '~/.hermes directory' };
|
|
311
|
+
}
|
|
312
|
+
const cli = tryCommand('hermes', ['--version'], 3000);
|
|
313
|
+
if (cli.ok) {
|
|
314
|
+
return { installed: true, method: 'hermes CLI', version: cli.stdout.trim() };
|
|
315
|
+
}
|
|
316
|
+
return { installed: false, method: '' };
|
|
317
|
+
},
|
|
318
|
+
models: [
|
|
319
|
+
{ name: 'model-agnostic' },
|
|
320
|
+
],
|
|
321
|
+
native_tools: ['execute_code', 'shell', 'file_read', 'file_write', 'web_search', 'delegate_task'],
|
|
322
|
+
mcp_support: true,
|
|
323
|
+
mcp_config_format: '~/.hermes/config.yaml',
|
|
324
|
+
skills_support: true,
|
|
325
|
+
skills_path_pattern: '~/.hermes/skills/ and .agents/skills/',
|
|
326
|
+
rules_support: false,
|
|
327
|
+
hooks_support: false,
|
|
328
|
+
instruction_file: 'AGENTS.md',
|
|
329
|
+
},
|
|
303
330
|
];
|
|
304
331
|
// ── Public API ─────────────────────────────────────────────────────────────────
|
|
305
332
|
/**
|
|
@@ -50,6 +50,8 @@ export const DEFAULT_HEALTH_CHECK_GRACE_MS = 60_000;
|
|
|
50
50
|
* declared `failed` with `silent_termination_no_evidence`. Default 30 min.
|
|
51
51
|
*/
|
|
52
52
|
export const DEFAULT_STALE_AFTER_MS = 30 * 60_000;
|
|
53
|
+
export const DEFAULT_DEAD_PID_READ_SWEEP_AGE_MS = 5 * 60_000;
|
|
54
|
+
export const DEFAULT_DEAD_PID_READ_SWEEP_LIMIT = 50;
|
|
53
55
|
const TERMINAL_STATUSES = new Set([
|
|
54
56
|
'completed', 'failed', 'cancelled', 'timed_out', 'interrupted',
|
|
55
57
|
]);
|
|
@@ -309,6 +311,134 @@ export function reconcileAgentRun(runId, cwd, options = {}) {
|
|
|
309
311
|
evidence, previous_status, current_status: run.status,
|
|
310
312
|
};
|
|
311
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* Read-path reconciliation for a `running` run whose tracked PID reads dead.
|
|
316
|
+
*
|
|
317
|
+
* IMPORTANT (pln#520): the tracked PID is NOT trustworthy. On Windows the
|
|
318
|
+
* ack-wrap spawn (shell:true) records the cmd.exe wrapper PID, not the real
|
|
319
|
+
* worker (cmd.exe -> claude.cmd -> node.exe), so a dead PID does NOT prove the
|
|
320
|
+
* worker died — empirically, 6 workers were cancelled here yet committed their
|
|
321
|
+
* work 4-7 min later. This function therefore NEVER cancels prematurely:
|
|
322
|
+
* - work evidence (commit / claim released / assignment completed)
|
|
323
|
+
* -> inferred `completed`;
|
|
324
|
+
* - past the stale threshold with a dead pid and still no evidence
|
|
325
|
+
* -> inferred `failed` (silent_termination_no_evidence). This MUST converge
|
|
326
|
+
* HERE: the canonical read path (entity-operations.ts) and the MCP pre-read
|
|
327
|
+
* sweep route `running` runs through THIS function, never through
|
|
328
|
+
* reconcileAgentRun, so deferring would leave a crashed run `running`
|
|
329
|
+
* forever (the trp#292 pattern);
|
|
330
|
+
* - otherwise (young, dead pid, no evidence yet) -> non-mutating health-check,
|
|
331
|
+
* leaving the run `running` so a worker behind an untrusted pid keeps its
|
|
332
|
+
* fair chance.
|
|
333
|
+
* Net vs pre-pln#520: a genuine silent death converges to `failed` after the
|
|
334
|
+
* stale window instead of an immediate (often false) `cancelled`.
|
|
335
|
+
*/
|
|
336
|
+
export function reconcileDeadPidRunningAgentRunAtRead(runId, cwd, options = {}) {
|
|
337
|
+
const run = loadAgentRun(runId, cwd);
|
|
338
|
+
if (!run) {
|
|
339
|
+
const evidence = {
|
|
340
|
+
age_ms: 0, has_post_start_commit: false, claim_released: false,
|
|
341
|
+
assignment_completed: false, process_alive: undefined,
|
|
342
|
+
};
|
|
343
|
+
return {
|
|
344
|
+
run_id: runId, action: 'no_op', reason: 'run not found', evidence,
|
|
345
|
+
previous_status: 'created', current_status: 'created',
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
const evidence = collectEvidence(run, cwd, { nowMs: options.nowMs });
|
|
349
|
+
if (run.status !== 'running') {
|
|
350
|
+
return {
|
|
351
|
+
run_id: run.id, action: 'no_op', reason: `run status is ${run.status}, not running`,
|
|
352
|
+
evidence, previous_status: run.status, current_status: run.status,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
if (evidence.process_alive !== false) {
|
|
356
|
+
return {
|
|
357
|
+
run_id: run.id, action: 'no_op',
|
|
358
|
+
reason: evidence.process_alive === true ? 'process alive' : 'pid liveness unknown',
|
|
359
|
+
evidence, previous_status: run.status, current_status: run.status,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
// pid reads dead — but the tracked pid is NOT trustworthy (see doc above),
|
|
363
|
+
// so a bare dead pid NEVER cancels. Evidence of real work wins; otherwise
|
|
364
|
+
// surface the uncertainty non-destructively and leave the run `running` for
|
|
365
|
+
// reconcileAgentRun's stale-threshold path to fail it only after a fair,
|
|
366
|
+
// evidence-based delay.
|
|
367
|
+
const actor = options.actor ?? 'reconciler';
|
|
368
|
+
if (anyCompletionEvidence(evidence)) {
|
|
369
|
+
try {
|
|
370
|
+
transitionAgentRun(run.id, 'completed', {
|
|
371
|
+
actor,
|
|
372
|
+
status_reason: `inferred=true; evidence: ${describeEvidence(evidence)}`,
|
|
373
|
+
}, cwd);
|
|
374
|
+
return {
|
|
375
|
+
run_id: run.id, action: 'inferred_completed',
|
|
376
|
+
reason: `inferred=true; ${describeEvidence(evidence)}`,
|
|
377
|
+
evidence, previous_status: run.status, current_status: 'completed',
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
catch (err) {
|
|
381
|
+
return {
|
|
382
|
+
run_id: run.id, action: 'no_op',
|
|
383
|
+
reason: `completion transition rejected: ${err instanceof Error ? err.message : String(err)}`,
|
|
384
|
+
evidence, previous_status: run.status, current_status: run.status,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// Stale + provably dead + still no evidence -> genuine silent failure. This
|
|
389
|
+
// MUST converge HERE: the canonical read path (entity-operations.ts) and the
|
|
390
|
+
// MCP pre-read sweep route `running` runs through this function, never
|
|
391
|
+
// through reconcileAgentRun, so deferring would leave a crashed run `running`
|
|
392
|
+
// forever (trp#292). The 30-min stale window — vs the immediate cancel before
|
|
393
|
+
// pln#520 — gives a worker behind an untrusted pid ample time to leave
|
|
394
|
+
// evidence first. Reported as `failed` (it died), not `cancelled`.
|
|
395
|
+
const stale = options.staleAfterMs ?? DEFAULT_STALE_AFTER_MS;
|
|
396
|
+
if (evidence.age_ms >= stale) {
|
|
397
|
+
try {
|
|
398
|
+
transitionAgentRun(run.id, 'failed', {
|
|
399
|
+
actor,
|
|
400
|
+
status_reason: 'silent_termination_no_evidence',
|
|
401
|
+
}, cwd);
|
|
402
|
+
return {
|
|
403
|
+
run_id: run.id, action: 'inferred_failed',
|
|
404
|
+
reason: 'silent_termination_no_evidence',
|
|
405
|
+
evidence, previous_status: run.status, current_status: 'failed',
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
return {
|
|
410
|
+
run_id: run.id, action: 'no_op',
|
|
411
|
+
reason: `failure transition rejected: ${err instanceof Error ? err.message : String(err)}`,
|
|
412
|
+
evidence, previous_status: run.status, current_status: run.status,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
emitUnverifiedEvent(run, evidence, actor, cwd);
|
|
417
|
+
return {
|
|
418
|
+
run_id: run.id, action: 'health_check_unverified',
|
|
419
|
+
reason: `pid_dead_untrusted_no_evidence (age=${Math.round(evidence.age_ms / 1000)}s) — awaiting evidence or stale window`,
|
|
420
|
+
evidence, previous_status: run.status, current_status: run.status,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
export function sweepDeadPidRunningAgentRunsAtRead(cwd, options = {}) {
|
|
424
|
+
const now = options.nowMs ?? Date.now();
|
|
425
|
+
const minAgeMs = options.staleAfterMs ?? DEFAULT_DEAD_PID_READ_SWEEP_AGE_MS;
|
|
426
|
+
const cutoff = now - minAgeMs;
|
|
427
|
+
const limit = options.limit ?? DEFAULT_DEAD_PID_READ_SWEEP_LIMIT;
|
|
428
|
+
const candidates = listAgentRuns(cwd, { status: 'running' })
|
|
429
|
+
.filter((run) => {
|
|
430
|
+
const lastEventAt = run.last_event_at ?? run.started_at ?? run.created_at;
|
|
431
|
+
const ts = new Date(lastEventAt).getTime();
|
|
432
|
+
return Number.isFinite(ts) && ts <= cutoff;
|
|
433
|
+
})
|
|
434
|
+
.sort((left, right) => {
|
|
435
|
+
const leftTs = new Date(left.last_event_at ?? left.started_at ?? left.created_at).getTime();
|
|
436
|
+
const rightTs = new Date(right.last_event_at ?? right.started_at ?? right.created_at).getTime();
|
|
437
|
+
return rightTs - leftTs;
|
|
438
|
+
})
|
|
439
|
+
.slice(0, limit);
|
|
440
|
+
return candidates.map((run) => reconcileDeadPidRunningAgentRunAtRead(run.id, cwd, options));
|
|
441
|
+
}
|
|
312
442
|
/**
|
|
313
443
|
* Reconcile every non-terminal agent_run matching `filter`. Useful for
|
|
314
444
|
* batch sweeps from `bclaw_assignment_events` or `brainclaw doctor --dispatch`.
|
|
@@ -19,7 +19,8 @@ import os from 'node:os';
|
|
|
19
19
|
* 9. Antigravity / Gemini CLI (ANTIGRAVITY_* env or ~/.gemini/antigravity/)
|
|
20
20
|
* 10. Continue (CONTINUE_*)
|
|
21
21
|
* 11. Roo Code (ROO_*)
|
|
22
|
-
* 12.
|
|
22
|
+
* 12. Hermes (HERMES_* or ~/.hermes/)
|
|
23
|
+
* 13. OpenClaw (~/.openclaw/ or OPENCLAW_*)
|
|
23
24
|
*/
|
|
24
25
|
export function detectAiAgent(env = process.env, homeDir = os.homedir()) {
|
|
25
26
|
// Explicit override
|
|
@@ -161,6 +162,21 @@ export function detectAiAgent(env = process.env, homeDir = os.homedir()) {
|
|
|
161
162
|
detection_source: env.VIBE_HOME ? 'VIBE_HOME env var' : '~/.vibe directory',
|
|
162
163
|
};
|
|
163
164
|
}
|
|
165
|
+
// Hermes Agent — supports MCP and skills from ~/.hermes/. Detect after
|
|
166
|
+
// editor/CLI agents with stronger session env vars to avoid stealing mixed
|
|
167
|
+
// shells where Hermes is merely installed.
|
|
168
|
+
if (env.HERMES_SESSION_ID || env.HERMES_AGENT || env.HERMES_HOME || fs.existsSync(path.join(homeDir, '.hermes'))) {
|
|
169
|
+
return {
|
|
170
|
+
name: 'hermes',
|
|
171
|
+
kind: 'autonomous',
|
|
172
|
+
trust_level: 'trusted',
|
|
173
|
+
detection_source: env.HERMES_SESSION_ID || env.HERMES_AGENT
|
|
174
|
+
? 'HERMES_* env var'
|
|
175
|
+
: env.HERMES_HOME
|
|
176
|
+
? 'HERMES_HOME env var'
|
|
177
|
+
: '~/.hermes directory',
|
|
178
|
+
};
|
|
179
|
+
}
|
|
164
180
|
return undefined;
|
|
165
181
|
}
|
|
166
182
|
/**
|