brainclaw 1.6.0 → 1.7.1
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 +33 -9
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/commands/mcp-schemas.generated.js +33 -0
- package/dist/commands/mcp.js +66 -31
- package/dist/commands/setup.js +64 -13
- package/dist/commands/switch.js +40 -8
- 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 +80 -15
- package/dist/core/ai-agent-detection.js +17 -1
- package/dist/core/claims.js +25 -3
- package/dist/core/dirty-scope.js +242 -0
- package/dist/core/facade-schema.js +17 -1
- package/dist/core/identity.js +40 -13
- package/dist/core/schema.js +2 -0
- package/dist/core/worktree.js +58 -0
- package/dist/facts.js +356 -3
- package/dist/facts.json +355 -2
- package/docs/cli.md +10 -7
- package/docs/concepts/troubleshooting.md +26 -0
- package/docs/index.md +4 -3
- package/docs/integrations/hermes.md +78 -0
- package/docs/integrations/overview.md +9 -7
- package/docs/mcp-schema-changelog.md +8 -1
- package/docs/quickstart-existing-project.md +1 -1
- package/package.json +5 -4
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
|
/**
|
|
@@ -311,6 +311,28 @@ export function reconcileAgentRun(runId, cwd, options = {}) {
|
|
|
311
311
|
evidence, previous_status, current_status: run.status,
|
|
312
312
|
};
|
|
313
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
|
+
*/
|
|
314
336
|
export function reconcileDeadPidRunningAgentRunAtRead(runId, cwd, options = {}) {
|
|
315
337
|
const run = loadAgentRun(runId, cwd);
|
|
316
338
|
if (!run) {
|
|
@@ -337,23 +359,66 @@ export function reconcileDeadPidRunningAgentRunAtRead(runId, cwd, options = {})
|
|
|
337
359
|
evidence, previous_status: run.status, current_status: run.status,
|
|
338
360
|
};
|
|
339
361
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
+
}
|
|
349
387
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
+
}
|
|
356
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
|
+
};
|
|
357
422
|
}
|
|
358
423
|
export function sweepDeadPidRunningAgentRunsAtRead(cwd, options = {}) {
|
|
359
424
|
const now = options.nowMs ?? Date.now();
|
|
@@ -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
|
/**
|
package/dist/core/claims.js
CHANGED
|
@@ -6,7 +6,7 @@ import { mutate } from './mutation-pipeline.js';
|
|
|
6
6
|
import { nowISO } from './ids.js';
|
|
7
7
|
import { JsonStore } from './json-store.js';
|
|
8
8
|
import { loadConfig } from './config.js';
|
|
9
|
-
import { createWorktree } from './worktree.js';
|
|
9
|
+
import { createWorktree, resetWorktreeToRef } from './worktree.js';
|
|
10
10
|
import { appendAuditEntry } from './audit.js';
|
|
11
11
|
import { refreshLiveCompanions } from '../commands/export.js';
|
|
12
12
|
import { loadSessionById } from './identity.js';
|
|
@@ -407,10 +407,28 @@ export function createCoordinatorClaim(options) {
|
|
|
407
407
|
const existingScopeClaim = listClaims(options.cwd).find((claim) => claim.status === 'active' && claim.scope === options.scope);
|
|
408
408
|
if (existingScopeClaim) {
|
|
409
409
|
if (existingScopeClaim.agent === options.agent) {
|
|
410
|
-
// Same agent already has this scope — reuse the claim (backward compat,
|
|
410
|
+
// Same agent already has this scope — reuse the claim (backward compat,
|
|
411
|
+
// same-agent multi-call). If the caller pinned a base ref, the reused
|
|
412
|
+
// worktree MUST be re-pointed to it; otherwise a dispatch that relied on
|
|
413
|
+
// the ref (e.g. the dirty-guard bypass) would run the worker on a stale
|
|
414
|
+
// worktree — the same silent false-negative the guard exists to prevent
|
|
415
|
+
// (pln#520 Tier 2 / codex r2).
|
|
416
|
+
let reuseWarning;
|
|
417
|
+
if (options.worktreeBaseRef) {
|
|
418
|
+
if (existingScopeClaim.worktree_path) {
|
|
419
|
+
const reset = resetWorktreeToRef(existingScopeClaim.worktree_path, options.worktreeBaseRef);
|
|
420
|
+
if (!reset.ok) {
|
|
421
|
+
reuseWarning = `Reused claim ${existingScopeClaim.id} pinned to ref "${options.worktreeBaseRef}": ${reset.stderr.trim()}`;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
reuseWarning = `Reused claim ${existingScopeClaim.id} has no worktree to pin to ref "${options.worktreeBaseRef}".`;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
411
428
|
return {
|
|
412
429
|
claimId: existingScopeClaim.id,
|
|
413
430
|
worktreePath: existingScopeClaim.worktree_path,
|
|
431
|
+
worktreeWarning: reuseWarning,
|
|
414
432
|
reusedExisting: true,
|
|
415
433
|
};
|
|
416
434
|
}
|
|
@@ -435,7 +453,11 @@ export function createCoordinatorClaim(options) {
|
|
|
435
453
|
sessionId: options.sessionId,
|
|
436
454
|
agent: options.agent,
|
|
437
455
|
baseRef: options.worktreeBaseRef,
|
|
438
|
-
|
|
456
|
+
// A pinned base ref implies the branch must be reset to it: createWorktree
|
|
457
|
+
// otherwise reuses a pre-existing feat/<scope> branch and ignores baseRef.
|
|
458
|
+
// Deriving it here (not only at the call site) keeps the invariant — "a
|
|
459
|
+
// pinned ref ⇒ the worktree reflects that ref" — owned by this chokepoint.
|
|
460
|
+
resetExistingBranch: options.resetExistingWorktreeBranch || Boolean(options.worktreeBaseRef),
|
|
439
461
|
});
|
|
440
462
|
}
|
|
441
463
|
catch (err) {
|