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.
Files changed (58) hide show
  1. package/README.md +5 -4
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli.js +124 -7
  4. package/dist/commands/bootstrap-loop.js +206 -0
  5. package/dist/commands/loop.js +156 -0
  6. package/dist/commands/loops-handlers.js +110 -55
  7. package/dist/commands/mcp-read-handlers.js +37 -0
  8. package/dist/commands/mcp-schemas.generated.js +33 -0
  9. package/dist/commands/mcp.js +661 -207
  10. package/dist/commands/questions.js +180 -0
  11. package/dist/commands/reply.js +190 -0
  12. package/dist/commands/session-end.js +105 -3
  13. package/dist/commands/session-start.js +32 -53
  14. package/dist/commands/setup.js +64 -13
  15. package/dist/commands/switch.js +17 -1
  16. package/dist/core/agent-capability.js +19 -0
  17. package/dist/core/agent-files.js +86 -0
  18. package/dist/core/agent-integrations.js +34 -0
  19. package/dist/core/agent-inventory.js +27 -0
  20. package/dist/core/agentrun-reconciler.js +130 -0
  21. package/dist/core/ai-agent-detection.js +17 -1
  22. package/dist/core/claims.js +54 -3
  23. package/dist/core/dirty-scope.js +242 -0
  24. package/dist/core/dispatch-status.js +219 -0
  25. package/dist/core/entity-operations.js +128 -9
  26. package/dist/core/execution-adapters.js +38 -2
  27. package/dist/core/facade-schema.js +71 -0
  28. package/dist/core/federation-cloud.js +27 -12
  29. package/dist/core/federation-materialize.js +57 -0
  30. package/dist/core/instruction-templates.js +2 -0
  31. package/dist/core/loops/bootstrap-acquire.js +195 -0
  32. package/dist/core/loops/facade-schema.js +68 -1
  33. package/dist/core/loops/hooks/bootstrap-write.js +144 -0
  34. package/dist/core/loops/hooks/notify-operator.js +148 -0
  35. package/dist/core/loops/hooks/survey-source-reader.js +256 -0
  36. package/dist/core/loops/index.js +8 -2
  37. package/dist/core/loops/next-expected.js +63 -0
  38. package/dist/core/loops/presets/bootstrap.js +75 -0
  39. package/dist/core/loops/presets/index.js +16 -0
  40. package/dist/core/loops/store.js +224 -4
  41. package/dist/core/loops/types.js +346 -1
  42. package/dist/core/loops/verbs.js +739 -6
  43. package/dist/core/schema.js +30 -2
  44. package/dist/core/state.js +62 -0
  45. package/dist/core/worktree.js +58 -0
  46. package/dist/facts.js +360 -5
  47. package/dist/facts.json +359 -4
  48. package/docs/cli.md +10 -7
  49. package/docs/concepts/dispatch-lifecycle.md +228 -0
  50. package/docs/concepts/loop-engine.md +55 -0
  51. package/docs/concepts/multi-agent-workflows.md +167 -166
  52. package/docs/concepts/troubleshooting.md +36 -2
  53. package/docs/index.md +4 -3
  54. package/docs/integrations/hermes.md +78 -0
  55. package/docs/integrations/overview.md +22 -18
  56. package/docs/mcp-schema-changelog.md +8 -1
  57. package/docs/quickstart-existing-project.md +1 -1
  58. package/package.json +5 -4
@@ -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
- export function parseAgentSelection(choice, detected) {
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 ? [detected] : [];
144
- return c.split(',').map((a) => a.trim()).filter((a) => ALL_KNOWN_AGENTS.includes(a));
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
- console.log(` ${i + 1}) ${a}${tag}`);
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 = detectedName ? 'detected' : 'all';
392
+ agentChoice = detectedSetupAgents.length > 0 ? 'detected' : 'all';
346
393
  }
347
394
  else {
348
- const defaultChoice = detectedName ? 'detected' : 'all';
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);
@@ -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
- const resolved = resolveProjectRef(projectRef, cwd);
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',
@@ -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. OpenClaw (~/.openclaw/ or OPENCLAW_*)
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
  /**