brainclaw 1.7.5 → 1.9.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 +28 -11
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +139 -13
- package/dist/commands/add-step.js +1 -1
- package/dist/commands/bootstrap.js +2 -26
- package/dist/commands/check-security-mcp.js +50 -33
- package/dist/commands/check-security.js +86 -43
- package/dist/commands/claim.js +22 -21
- package/dist/commands/confirm.js +26 -0
- package/dist/commands/context-diff.js +1 -1
- package/dist/commands/dispatch-watch.js +142 -0
- package/dist/commands/doctor.js +113 -2
- package/dist/commands/estimation-report.js +115 -16
- package/dist/commands/harvest.js +502 -16
- package/dist/commands/init.js +123 -21
- package/dist/commands/loops-handlers.js +4 -0
- package/dist/commands/mcp-read-handlers.js +198 -29
- package/dist/commands/mcp.js +615 -92
- package/dist/commands/memory.js +21 -17
- package/dist/commands/migrate.js +81 -17
- package/dist/commands/prune.js +78 -4
- package/dist/commands/reflect.js +26 -20
- package/dist/commands/register-agent.js +57 -1
- package/dist/commands/repair.js +20 -0
- package/dist/commands/session-end.js +15 -6
- package/dist/commands/session-start.js +18 -1
- package/dist/commands/setup-security.js +39 -18
- package/dist/commands/setup.js +26 -27
- package/dist/commands/stale.js +16 -2
- package/dist/commands/uninstall.js +126 -34
- package/dist/commands/update-step.js +6 -0
- package/dist/commands/worktree.js +60 -0
- package/dist/core/actions.js +12 -3
- package/dist/core/agent-capability.js +11 -13
- package/dist/core/agent-files.js +844 -547
- package/dist/core/agent-integrations.js +0 -3
- package/dist/core/agent-inventory.js +67 -0
- package/dist/core/agent-registry.js +163 -29
- package/dist/core/agentrun-reconciler.js +33 -2
- package/dist/core/agentruns.js +7 -1
- package/dist/core/ai-agent-detection.js +31 -44
- package/dist/core/archival.js +15 -9
- package/dist/core/assignment-reconciler.js +56 -0
- package/dist/core/assignment-sweeper.js +127 -4
- package/dist/core/assignments.js +69 -11
- package/dist/core/bootstrap.js +233 -67
- package/dist/core/brainclaw-version.js +22 -0
- package/dist/core/candidates.js +21 -1
- package/dist/core/claims.js +313 -150
- package/dist/core/config.js +6 -1
- package/dist/core/context-diff.js +148 -20
- package/dist/core/context.js +129 -8
- package/dist/core/coordination.js +22 -3
- package/dist/core/dispatch-status.js +109 -5
- package/dist/core/dispatcher.js +65 -11
- package/dist/core/entity-operations.js +45 -24
- package/dist/core/entity-registry.js +31 -5
- package/dist/core/event-log.js +138 -21
- package/dist/core/events/checkpoint.js +258 -0
- package/dist/core/events/genesis.js +220 -0
- package/dist/core/events/journal.js +507 -0
- package/dist/core/events/materialize.js +126 -0
- package/dist/core/events/registry-post-image.js +110 -0
- package/dist/core/events/verify.js +109 -0
- package/dist/core/execution-adapters.js +23 -0
- package/dist/core/execution.js +25 -0
- package/dist/core/facade-schema.js +48 -0
- package/dist/core/gc-semantic.js +130 -5
- package/dist/core/handoff-snapshot.js +68 -0
- package/dist/core/ids.js +19 -8
- package/dist/core/instruction-templates.js +34 -115
- package/dist/core/io.js +39 -3
- package/dist/core/json-store.js +10 -1
- package/dist/core/lock.js +153 -28
- package/dist/core/loops/bootstrap-acquire.js +25 -1
- package/dist/core/loops/facade-schema.js +2 -0
- package/dist/core/loops/hooks/survey-signals-baseline.js +36 -0
- package/dist/core/loops/index.js +1 -0
- package/dist/core/loops/presets/bootstrap.js +7 -0
- package/dist/core/loops/store.js +17 -0
- package/dist/core/loops/verbs.js +24 -1
- package/dist/core/markdown.js +8 -76
- package/dist/core/mcp-command-resolution.js +245 -0
- package/dist/core/memory-compactor.js +5 -3
- package/dist/core/memory-lifecycle.js +282 -0
- package/dist/core/merge-risk.js +150 -0
- package/dist/core/messaging.js +8 -1
- package/dist/core/migration.js +11 -1
- package/dist/core/observer-mode.js +26 -0
- package/dist/core/operations/memory-mutation.js +90 -65
- package/dist/core/operations/plan.js +27 -1
- package/dist/core/protocol-skills.js +210 -0
- package/dist/core/reflection-safety.js +6 -7
- package/dist/core/reputation.js +84 -2
- package/dist/core/runtime-signals.js +71 -9
- package/dist/core/runtime.js +84 -1
- package/dist/core/schema.js +125 -0
- package/dist/core/security-detectors.js +125 -0
- package/dist/core/security-extract.js +189 -0
- package/dist/core/security-guard.js +107 -29
- package/dist/core/security-packages.js +121 -0
- package/dist/core/security-scoring.js +76 -9
- package/dist/core/security.js +34 -2
- package/dist/core/sequence.js +11 -2
- package/dist/core/setup-flow.js +141 -13
- package/dist/core/spawn-check.js +110 -4
- package/dist/core/staleness.js +109 -1
- package/dist/core/state.js +250 -54
- package/dist/core/store-resolution.js +19 -5
- package/dist/core/worktree.js +169 -7
- package/dist/facts.js +8 -8
- package/dist/facts.json +7 -7
- package/docs/PROTOCOL.md +223 -0
- package/docs/cli.md +11 -10
- package/docs/concepts/coordinator-runbook.md +129 -0
- package/docs/concepts/dispatch-lifecycle.md +17 -0
- package/docs/concepts/event-log-store-critique-A.md +333 -0
- package/docs/concepts/event-log-store-critique-B.md +353 -0
- package/docs/concepts/event-log-store-phase0-measurements.md +58 -0
- package/docs/concepts/event-log-store-proposal-A.md +365 -0
- package/docs/concepts/event-log-store-proposal-B.md +404 -0
- package/docs/concepts/event-log-store.md +928 -0
- package/docs/concepts/identity-model-proposal.md +371 -0
- package/docs/concepts/memory.md +5 -4
- package/docs/concepts/observer-protocol.md +361 -0
- package/docs/concepts/parallel-merge-protocol.md +71 -0
- package/docs/concepts/plans-and-claims.md +43 -0
- package/docs/concepts/skills.md +78 -0
- package/docs/concepts/workspace-bootstrapping.md +61 -0
- package/docs/integrations/agents.md +4 -4
- package/docs/integrations/cline.md +10 -11
- package/docs/integrations/codex.md +2 -2
- package/docs/integrations/continue.md +5 -5
- package/docs/integrations/copilot.md +14 -12
- package/docs/integrations/openclaw.md +7 -6
- package/docs/integrations/overview.md +7 -7
- package/docs/integrations/roo.md +3 -3
- package/docs/integrations/windsurf.md +6 -6
- package/docs/mcp-schema-changelog.md +51 -20
- package/docs/quickstart.md +48 -47
- package/docs/security.md +174 -15
- package/docs/storage.md +4 -2
- package/package.json +8 -6
|
@@ -51,18 +51,15 @@ const DEFAULT_SURFACES = {
|
|
|
51
51
|
],
|
|
52
52
|
'windsurf': [
|
|
53
53
|
{ kind: 'instructions', location: 'workspace', path: '.windsurfrules' },
|
|
54
|
-
{ kind: 'hook', location: 'workspace', path: '.windsurfrules' },
|
|
55
54
|
{ kind: 'mcp', location: 'machine', path: '.codeium/windsurf/mcp_config.json' },
|
|
56
55
|
{ kind: 'rule', location: 'workspace', path: '.windsurf/rules/brainclaw.md' },
|
|
57
56
|
],
|
|
58
57
|
'cline': [
|
|
59
58
|
{ kind: 'instructions', location: 'workspace', path: '.clinerules/brainclaw.md' },
|
|
60
|
-
{ kind: 'hook', location: 'workspace', path: '.clinerules/brainclaw.md' },
|
|
61
59
|
{ kind: 'mcp', location: 'workspace', path: '.vscode/cline_mcp_settings.json' },
|
|
62
60
|
],
|
|
63
61
|
'codex': [
|
|
64
62
|
{ kind: 'instructions', location: 'workspace', path: 'AGENTS.md' },
|
|
65
|
-
{ kind: 'hook', location: 'workspace', path: 'AGENTS.md' },
|
|
66
63
|
{ kind: 'mcp', location: 'machine', path: '.codex/config.toml' },
|
|
67
64
|
{ kind: 'skill', location: 'workspace', path: '.agents/skills/brainclaw/SKILL.md' },
|
|
68
65
|
],
|
|
@@ -336,6 +336,51 @@ const AGENT_DEFINITIONS = [
|
|
|
336
336
|
hooks_support: false,
|
|
337
337
|
instruction_file: '.continue/rules/',
|
|
338
338
|
},
|
|
339
|
+
{
|
|
340
|
+
name: 'openclaw',
|
|
341
|
+
detect: (home, env) => {
|
|
342
|
+
if (env.OPENCLAW_SESSION_ID || env.OPENCLAW_AGENT) {
|
|
343
|
+
return { installed: true, method: 'OPENCLAW_* env' };
|
|
344
|
+
}
|
|
345
|
+
if (fs.existsSync(path.join(home, '.openclaw'))) {
|
|
346
|
+
return { installed: true, method: '~/.openclaw directory' };
|
|
347
|
+
}
|
|
348
|
+
return { installed: false, method: '' };
|
|
349
|
+
},
|
|
350
|
+
models: [
|
|
351
|
+
{ name: 'model-agnostic' },
|
|
352
|
+
],
|
|
353
|
+
native_tools: ['shell', 'file_read', 'file_write', 'file_edit'],
|
|
354
|
+
mcp_support: true,
|
|
355
|
+
mcp_config_format: '~/.openclaw/mcp.json',
|
|
356
|
+
skills_support: false,
|
|
357
|
+
rules_support: false,
|
|
358
|
+
hooks_support: false,
|
|
359
|
+
instruction_file: 'AGENTS.md',
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
name: 'mistral-vibe',
|
|
363
|
+
detect: (home, env) => {
|
|
364
|
+
if (env.VIBE_HOME?.trim()) {
|
|
365
|
+
return { installed: true, method: 'VIBE_HOME env' };
|
|
366
|
+
}
|
|
367
|
+
if (fs.existsSync(path.join(home, '.vibe'))) {
|
|
368
|
+
return { installed: true, method: '~/.vibe directory' };
|
|
369
|
+
}
|
|
370
|
+
return { installed: false, method: '' };
|
|
371
|
+
},
|
|
372
|
+
models: [
|
|
373
|
+
{ name: 'devstral', context_window: 256000 },
|
|
374
|
+
{ name: 'mistral-large', context_window: 128000 },
|
|
375
|
+
],
|
|
376
|
+
native_tools: ['shell', 'file_read', 'file_write', 'file_edit'],
|
|
377
|
+
mcp_support: true,
|
|
378
|
+
mcp_config_format: '~/.vibe/mcp.json',
|
|
379
|
+
skills_support: false,
|
|
380
|
+
rules_support: false,
|
|
381
|
+
hooks_support: false,
|
|
382
|
+
instruction_file: 'AGENTS.md',
|
|
383
|
+
},
|
|
339
384
|
{
|
|
340
385
|
name: 'hermes',
|
|
341
386
|
detect: (home, env) => {
|
|
@@ -437,6 +482,28 @@ export function loadAgentInventory() {
|
|
|
437
482
|
return undefined;
|
|
438
483
|
}
|
|
439
484
|
}
|
|
485
|
+
/**
|
|
486
|
+
* Whether an agent is installed on this machine according to the saved
|
|
487
|
+
* inventory (the single source of truth for "installed" since the
|
|
488
|
+
* detection/inventory split — pln#562 step 1).
|
|
489
|
+
*
|
|
490
|
+
* Returns:
|
|
491
|
+
* - true / false when the inventory tracks the agent
|
|
492
|
+
* - undefined when no inventory exists yet or the agent is not tracked
|
|
493
|
+
* (callers must NOT treat undefined as "not installed")
|
|
494
|
+
*
|
|
495
|
+
* The inventory never mints identity — this is a consultation helper for
|
|
496
|
+
* warning paths only.
|
|
497
|
+
*/
|
|
498
|
+
export function isAgentInstalledPerInventory(agentName, inventory = loadAgentInventory()) {
|
|
499
|
+
if (!inventory)
|
|
500
|
+
return undefined;
|
|
501
|
+
const normalized = agentName.trim().toLowerCase();
|
|
502
|
+
const entry = inventory.agents.find((a) => a.name === normalized);
|
|
503
|
+
if (!entry)
|
|
504
|
+
return undefined;
|
|
505
|
+
return entry.installed;
|
|
506
|
+
}
|
|
440
507
|
/**
|
|
441
508
|
* Render a human-readable summary of the agent inventory.
|
|
442
509
|
*/
|
|
@@ -2,7 +2,8 @@ import crypto from 'node:crypto';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { isKnownAgent, getCapabilityProfile } from './agent-capability.js';
|
|
5
|
+
import { isKnownAgent, getCapabilityProfile, resolveAgentAlias } from './agent-capability.js';
|
|
6
|
+
import { isAgentInstalledPerInventory } from './agent-inventory.js';
|
|
6
7
|
import { detectAiAgent } from './ai-agent-detection.js';
|
|
7
8
|
import { loadConfig, saveConfig } from './config.js';
|
|
8
9
|
import { nowISO } from './ids.js';
|
|
@@ -10,6 +11,7 @@ import { MEMORY_DIR, memoryExists, resolveEntityDir } from './io.js';
|
|
|
10
11
|
import { JsonStore } from './json-store.js';
|
|
11
12
|
import { AgentIdentityDocumentSchema, } from './schema.js';
|
|
12
13
|
import { logger } from './logger.js';
|
|
14
|
+
import { isObserverMode } from './observer-mode.js';
|
|
13
15
|
// agents/ stays at top level in entity model (already entity-aligned)
|
|
14
16
|
const TRUST_ORDER = ['observer', 'contributor', 'trusted', 'curator'];
|
|
15
17
|
export class AgentIdentityResolutionError extends Error {
|
|
@@ -55,8 +57,13 @@ function agentStore(cwd, preferredDirName) {
|
|
|
55
57
|
sort: (a, b) => a.created_at.localeCompare(b.created_at) || a.agent_name.localeCompare(b.agent_name),
|
|
56
58
|
});
|
|
57
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Canonical agent name: lowercased, trimmed, and alias-resolved at the
|
|
62
|
+
* registry level (pln#562 step 2) — 'copilot' and 'github-copilot' are ONE
|
|
63
|
+
* identity, not two. All registry lookups and writes go through this.
|
|
64
|
+
*/
|
|
58
65
|
export function normalizeAgentName(agentName) {
|
|
59
|
-
return agentName.trim().toLowerCase();
|
|
66
|
+
return resolveAgentAlias(agentName.trim().toLowerCase());
|
|
60
67
|
}
|
|
61
68
|
function normalizeCapability(capability) {
|
|
62
69
|
const normalized = capability.trim().toLowerCase();
|
|
@@ -86,9 +93,47 @@ function codexHome(env = process.env) {
|
|
|
86
93
|
const explicit = env.CODEX_HOME?.trim();
|
|
87
94
|
return explicit && explicit.length > 0 ? explicit : path.join(os.homedir(), '.codex');
|
|
88
95
|
}
|
|
89
|
-
|
|
96
|
+
/**
|
|
97
|
+
* ed25519 identity keys (pln#562 step 5).
|
|
98
|
+
*
|
|
99
|
+
* RESERVED for the federated identity model — these keys are not consumed by
|
|
100
|
+
* any verification path today; the identity proposal (origin signing)
|
|
101
|
+
* activates them. Do NOT delete them as debris.
|
|
102
|
+
*
|
|
103
|
+
* Private keys live under the NEUTRAL brainclaw home (~/.brainclaw/keys/),
|
|
104
|
+
* not under ~/.codex/: agent identity belongs to brainclaw, and parking
|
|
105
|
+
* private key material inside another vendor's config directory both
|
|
106
|
+
* misattributes it and exposes it to that vendor's tooling/sync.
|
|
107
|
+
*/
|
|
108
|
+
function agentKeyPath(agentId) {
|
|
109
|
+
return path.join(os.homedir(), MEMORY_DIR, 'keys', `${agentId}.ed25519.pem`);
|
|
110
|
+
}
|
|
111
|
+
/** Pre-step-5 location (inside CODEX_HOME) — read for one-time migration. */
|
|
112
|
+
function legacyAgentKeyPath(agentId, env = process.env) {
|
|
90
113
|
return path.join(codexHome(env), 'brainclaw', 'keys', `${agentId}.ed25519.pem`);
|
|
91
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Move a key file from the legacy ~/.codex location to the neutral path.
|
|
117
|
+
* Best-effort: a failed unlink leaves a duplicate, never a missing key.
|
|
118
|
+
*/
|
|
119
|
+
function migrateLegacyAgentKey(agentId, env = process.env) {
|
|
120
|
+
const legacy = legacyAgentKeyPath(agentId, env);
|
|
121
|
+
const target = agentKeyPath(agentId);
|
|
122
|
+
if (fs.existsSync(target) || !fs.existsSync(legacy))
|
|
123
|
+
return;
|
|
124
|
+
try {
|
|
125
|
+
ensureParentDir(target);
|
|
126
|
+
fs.copyFileSync(legacy, target);
|
|
127
|
+
try {
|
|
128
|
+
fs.unlinkSync(legacy);
|
|
129
|
+
}
|
|
130
|
+
catch { /* duplicate is safe */ }
|
|
131
|
+
logger.debug(`Migrated agent identity key ${agentId} from ${legacy} to ${target}`);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
logger.debug('Failed to migrate legacy agent key:', err);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
92
137
|
function ensureParentDir(filepath) {
|
|
93
138
|
const dir = path.dirname(filepath);
|
|
94
139
|
if (!fs.existsSync(dir)) {
|
|
@@ -99,7 +144,8 @@ function fingerprintPublicKey(publicKey) {
|
|
|
99
144
|
return crypto.createHash('sha256').update(publicKey).digest('hex');
|
|
100
145
|
}
|
|
101
146
|
function buildIdentityKey(agentId, env = process.env, forceRegenerate = false) {
|
|
102
|
-
|
|
147
|
+
migrateLegacyAgentKey(agentId, env);
|
|
148
|
+
const filepath = agentKeyPath(agentId);
|
|
103
149
|
const createdAt = nowISO();
|
|
104
150
|
let publicKeyPem;
|
|
105
151
|
if (!forceRegenerate && fs.existsSync(filepath)) {
|
|
@@ -152,6 +198,27 @@ export function findAgentIdentityById(agentId, cwd, preferredDirName) {
|
|
|
152
198
|
export function registerAgentIdentity(input) {
|
|
153
199
|
const normalizedCapabilities = normalizeCapabilities(input.capabilities);
|
|
154
200
|
const existing = findAgentIdentityByName(input.agentName, input.cwd, input.preferredDirName);
|
|
201
|
+
// Observer mode (BRAINCLAW_OBSERVER=1) refuses to mint or mutate identity
|
|
202
|
+
// on the disk. A dashboard is not an agent — it must never auto-register
|
|
203
|
+
// (the 2026-06-10 leak where the VS Code extension impersonated whichever
|
|
204
|
+
// shell-parent agent VS Code was launched from). Return existing read-only,
|
|
205
|
+
// or a transient synthetic identity that callers can use without persisting.
|
|
206
|
+
if (isObserverMode()) {
|
|
207
|
+
if (existing)
|
|
208
|
+
return existing;
|
|
209
|
+
const normalizedNewName = normalizeAgentName(input.agentName);
|
|
210
|
+
return {
|
|
211
|
+
schema_version: 2,
|
|
212
|
+
version: 1,
|
|
213
|
+
agent_id: generateAgentId(),
|
|
214
|
+
agent_name: normalizedNewName,
|
|
215
|
+
created_at: nowISO(),
|
|
216
|
+
kind: input.kind ?? 'unknown',
|
|
217
|
+
trust_level: input.trustLevel ?? 'observer',
|
|
218
|
+
capabilities: normalizedCapabilities,
|
|
219
|
+
...(input.contextProfile ? { context_profile: input.contextProfile } : {}),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
155
222
|
if (existing) {
|
|
156
223
|
let updated = existing;
|
|
157
224
|
if (input.kind && existing.kind !== input.kind) {
|
|
@@ -180,11 +247,22 @@ export function registerAgentIdentity(input) {
|
|
|
180
247
|
}
|
|
181
248
|
return updated;
|
|
182
249
|
}
|
|
250
|
+
// Identity hardening (pln#562 step 1): a NEW identity claiming the name of
|
|
251
|
+
// an agent the inventory knows is NOT installed on this machine is suspect.
|
|
252
|
+
// Warn (the inventory is consultative) — it never blocks or mints identity.
|
|
253
|
+
const normalizedNewName = normalizeAgentName(input.agentName);
|
|
254
|
+
try {
|
|
255
|
+
if (isAgentInstalledPerInventory(normalizedNewName) === false) {
|
|
256
|
+
logger.warn(`Registering identity '${normalizedNewName}' but the agent inventory reports it is not installed on this machine. `
|
|
257
|
+
+ 'Verify the claimed identity or refresh the inventory.');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
catch { /* inventory consultation is best-effort */ }
|
|
183
261
|
let created = {
|
|
184
262
|
schema_version: 2,
|
|
185
263
|
version: 1,
|
|
186
264
|
agent_id: generateAgentId(),
|
|
187
|
-
agent_name:
|
|
265
|
+
agent_name: normalizedNewName,
|
|
188
266
|
created_at: nowISO(),
|
|
189
267
|
kind: input.kind ?? 'unknown',
|
|
190
268
|
trust_level: input.trustLevel ?? 'contributor',
|
|
@@ -212,34 +290,24 @@ export function resolveCurrentAgentIdentity(cwd, preferredDirName, homeDir) {
|
|
|
212
290
|
return byEnvName;
|
|
213
291
|
}
|
|
214
292
|
// Auto-detect from native agent env vars (e.g. CLAUDECODE, CURSOR_TRACE_ID, CODEX_THREAD_ID).
|
|
215
|
-
// If detected agent is not registered, auto-register it as trusted agent.
|
|
216
293
|
// This is the primary identification path for MCP servers and CLI hooks.
|
|
217
|
-
|
|
294
|
+
//
|
|
295
|
+
// pln#562 step 2 — registration is an EXPLICIT act (setup selection, session
|
|
296
|
+
// start, dispatcher spawn). Resolution is a read path and must never mint an
|
|
297
|
+
// identity as a side effect; a detected-but-unregistered agent resolves to
|
|
298
|
+
// undefined and the caller decides whether to register explicitly.
|
|
299
|
+
const detected = detectAiAgent(process.env);
|
|
218
300
|
if (detected) {
|
|
219
301
|
// If the detected name matches an explicit env var that was already tried
|
|
220
|
-
// and not found,
|
|
302
|
+
// and not found, the caller expects a "not registered" error.
|
|
221
303
|
if (normalizeAgentName(detected.name) === normalizeAgentName(envAgentName)) {
|
|
222
304
|
return undefined;
|
|
223
305
|
}
|
|
224
306
|
const byDetected = findAgentIdentityByName(detected.name, cwd, preferredDirName);
|
|
225
307
|
if (byDetected)
|
|
226
308
|
return byDetected;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
try {
|
|
230
|
-
const autoRegistered = registerAgentIdentity({
|
|
231
|
-
agentName: normalizeAgentName(detected.name),
|
|
232
|
-
kind: detected.kind,
|
|
233
|
-
trustLevel: detected.trust_level,
|
|
234
|
-
cwd,
|
|
235
|
-
preferredDirName,
|
|
236
|
-
});
|
|
237
|
-
logger.debug(`Auto-registered detected agent: ${detected.name} (${autoRegistered.agent_id})`);
|
|
238
|
-
return autoRegistered;
|
|
239
|
-
}
|
|
240
|
-
catch {
|
|
241
|
-
// Non-fatal: registration may fail if store is read-only
|
|
242
|
-
}
|
|
309
|
+
logger.debug(`Detected agent '${detected.name}' is not registered; read-path resolution does not auto-register `
|
|
310
|
+
+ '(register via setup, session start, or dispatch).');
|
|
243
311
|
}
|
|
244
312
|
// config.current_agent is NOT used for identity resolution — it's a singleton global
|
|
245
313
|
// that gets overwritten by whichever agent last ran register-agent --set-current.
|
|
@@ -370,10 +438,12 @@ export function resolveOrAutoRegisterAgentIdentity(options = {}) {
|
|
|
370
438
|
catch (err) {
|
|
371
439
|
if (!(err instanceof AgentIdentityResolutionError))
|
|
372
440
|
throw err;
|
|
373
|
-
// Last-resort: derive a name from explicit arg or
|
|
374
|
-
//
|
|
441
|
+
// Last-resort: derive a name from explicit arg, env, or runtime detection
|
|
442
|
+
// and auto-register. Session start is an EXPLICIT act (pln#562 step 2), so
|
|
443
|
+
// it is allowed to register — unlike read-path resolution, which is not.
|
|
375
444
|
const candidateName = options.agentName?.trim()
|
|
376
|
-
|| (options.allowEnv !== false ? resolveEnvAgentName(options.env ?? process.env) : undefined)
|
|
445
|
+
|| (options.allowEnv !== false ? resolveEnvAgentName(options.env ?? process.env) : undefined)
|
|
446
|
+
|| detectAiAgent(options.env ?? process.env)?.name;
|
|
377
447
|
if (!candidateName)
|
|
378
448
|
throw err;
|
|
379
449
|
const normalizedName = normalizeAgentName(candidateName);
|
|
@@ -467,11 +537,11 @@ export function resolveCurrentModel(cwd) {
|
|
|
467
537
|
* Note: config.current_agent is intentionally NOT used here — it's a singleton
|
|
468
538
|
* global that causes cross-agent confusion in multi-agent setups.
|
|
469
539
|
*/
|
|
470
|
-
export function resolveCurrentAgentName(cwd,
|
|
540
|
+
export function resolveCurrentAgentName(cwd, _homeDir) {
|
|
471
541
|
const fromEnv = (process.env.BRAINCLAW_AGENT_NAME ?? process.env.BRAINCLAW_AGENT)?.trim();
|
|
472
542
|
if (fromEnv)
|
|
473
543
|
return fromEnv;
|
|
474
|
-
const detected = detectAiAgent(process.env
|
|
544
|
+
const detected = detectAiAgent(process.env);
|
|
475
545
|
if (detected)
|
|
476
546
|
return detected.name;
|
|
477
547
|
return process.env.USER ?? process.env.USERNAME ?? 'unknown';
|
|
@@ -559,4 +629,68 @@ export function agentCanCurate(agentNameOrId, cwd) {
|
|
|
559
629
|
const level = getAgentTrustLevel(agentNameOrId, cwd);
|
|
560
630
|
return level === 'curator';
|
|
561
631
|
}
|
|
632
|
+
// ── Debris identity cleanup (pln#562 step 2) ────────────────────────────────
|
|
633
|
+
/**
|
|
634
|
+
* Identity names known to be registration debris: test fixtures and
|
|
635
|
+
* model-as-identity artifacts that leaked into real stores through the old
|
|
636
|
+
* permissive auto-registration paths.
|
|
637
|
+
*/
|
|
638
|
+
export const DEBRIS_AGENT_NAMES = ['testuser', 'contributor-bot', 'claude-sonnet'];
|
|
639
|
+
/**
|
|
640
|
+
* List identities that look like registration debris:
|
|
641
|
+
* - names on the known-debris list (test fixtures, model-as-identity)
|
|
642
|
+
* - identities stored under an alias of a canonical agent name (e.g. a
|
|
643
|
+
* 'copilot' document now shadowed by the registry-level alias merge)
|
|
644
|
+
*
|
|
645
|
+
* Read-only — cleanup is a separate, guarded act (removeAgentIdentity).
|
|
646
|
+
*/
|
|
647
|
+
export function listDebrisAgentIdentities(cwd, preferredDirName) {
|
|
648
|
+
const debris = [];
|
|
649
|
+
for (const identity of listAgentIdentities(cwd, preferredDirName)) {
|
|
650
|
+
const stored = identity.agent_name.trim().toLowerCase();
|
|
651
|
+
if (DEBRIS_AGENT_NAMES.includes(stored)) {
|
|
652
|
+
debris.push({ identity, reason: `'${stored}' is a known debris identity name` });
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
const canonical = resolveAgentAlias(stored);
|
|
656
|
+
if (canonical !== stored) {
|
|
657
|
+
debris.push({
|
|
658
|
+
identity,
|
|
659
|
+
reason: `'${stored}' is an alias of '${canonical}' — superseded by the registry-level alias merge`,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return debris;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Remove a registered agent identity — guarded, never silent.
|
|
667
|
+
*
|
|
668
|
+
* Refuses unless the identity is flagged as debris (listDebrisAgentIdentities)
|
|
669
|
+
* or the caller passes force:true. Curator identities are never removed
|
|
670
|
+
* without force. Returns the removed document so callers can report exactly
|
|
671
|
+
* what was deleted.
|
|
672
|
+
*/
|
|
673
|
+
export function removeAgentIdentity(agentNameOrId, options = {}) {
|
|
674
|
+
const { cwd, preferredDirName, force } = options;
|
|
675
|
+
const identity = findAgentIdentityById(agentNameOrId, cwd, preferredDirName)
|
|
676
|
+
?? findAgentIdentityByName(agentNameOrId, cwd, preferredDirName)
|
|
677
|
+
// Alias-debris docs are unreachable via normalized name lookup — match the raw stored name.
|
|
678
|
+
?? listAgentIdentities(cwd, preferredDirName).find((a) => a.agent_name.trim().toLowerCase() === agentNameOrId.trim().toLowerCase());
|
|
679
|
+
if (!identity) {
|
|
680
|
+
throw new AgentIdentityResolutionError(`Agent '${agentNameOrId}' not found.`, { agent_name: agentNameOrId });
|
|
681
|
+
}
|
|
682
|
+
if (!force) {
|
|
683
|
+
if (identity.trust_level === 'curator') {
|
|
684
|
+
throw new AgentTrustError(`Refusing to remove curator identity '${identity.agent_name}' without force.`, { agent_id: identity.agent_id, agent_name: identity.agent_name });
|
|
685
|
+
}
|
|
686
|
+
const isDebris = listDebrisAgentIdentities(cwd, preferredDirName)
|
|
687
|
+
.some((d) => d.identity.agent_id === identity.agent_id);
|
|
688
|
+
if (!isDebris) {
|
|
689
|
+
throw new AgentIdentityResolutionError(`Refusing to remove '${identity.agent_name}': not a known debris identity. Pass force to override.`, { agent_id: identity.agent_id, agent_name: identity.agent_name });
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
agentStore(cwd, preferredDirName).delete(identity.agent_id);
|
|
693
|
+
logger.debug(`Removed agent identity ${identity.agent_name} (${identity.agent_id})`);
|
|
694
|
+
return identity;
|
|
695
|
+
}
|
|
562
696
|
//# sourceMappingURL=agent-registry.js.map
|
|
@@ -169,7 +169,10 @@ export function collectEvidence(run, cwd, options) {
|
|
|
169
169
|
try {
|
|
170
170
|
completed_signal = signalExists(signalRoot, run.assignment_id, 'completed');
|
|
171
171
|
failed_signal = signalExists(signalRoot, run.assignment_id, 'failed');
|
|
172
|
-
|
|
172
|
+
// sprint 1.5: also read the worktree-local heartbeat — the only location a
|
|
173
|
+
// sandboxed worker can write (the project-root signal dir is outside its
|
|
174
|
+
// writable roots).
|
|
175
|
+
const hb = readHeartbeat(signalRoot, run.assignment_id, run.worktree_path);
|
|
173
176
|
heartbeat_exists = hb.exists;
|
|
174
177
|
if (hb.exists && hb.mtimeMs !== undefined)
|
|
175
178
|
heartbeat_age_ms = now - hb.mtimeMs;
|
|
@@ -268,7 +271,28 @@ function describeEvidence(evidence) {
|
|
|
268
271
|
return reasons.join(' + ');
|
|
269
272
|
}
|
|
270
273
|
// ── Synthetic event for unverified spawns ──────────────────────────────────
|
|
274
|
+
/**
|
|
275
|
+
* Per-run throttle for "delivered_but_unverified" events (pln#558 step 5).
|
|
276
|
+
*
|
|
277
|
+
* Before this throttle every reconciliation pass during the health-check
|
|
278
|
+
* window produced a fresh runtime_event file. With the VS Code extension
|
|
279
|
+
* polling kind='board' every 30s, both reconciliation passes firing per
|
|
280
|
+
* poll, and several non-terminal runs in flight, the store accumulated
|
|
281
|
+
* ~120 of these files per hour per run — all writing under the mutation
|
|
282
|
+
* lock, none ever surfaced to the UI.
|
|
283
|
+
*
|
|
284
|
+
* Surfacing the uncertainty once per window is enough; the event content
|
|
285
|
+
* is monotonic (age increases) so re-emitting adds no information.
|
|
286
|
+
*/
|
|
287
|
+
const UNVERIFIED_EVENT_THROTTLE_MS = 5 * 60_000;
|
|
288
|
+
const lastUnverifiedEmitAt = new Map();
|
|
271
289
|
function emitUnverifiedEvent(run, evidence, actor, cwd) {
|
|
290
|
+
const now = Date.now();
|
|
291
|
+
const last = lastUnverifiedEmitAt.get(run.id);
|
|
292
|
+
if (last !== undefined && now - last < UNVERIFIED_EVENT_THROTTLE_MS) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
lastUnverifiedEmitAt.set(run.id, now);
|
|
272
296
|
try {
|
|
273
297
|
createRuntimeEvent({
|
|
274
298
|
agent: actor,
|
|
@@ -551,6 +575,13 @@ export function reconcileDeadPidRunningAgentRunAtRead(runId, cwd, options = {})
|
|
|
551
575
|
// window (trp#292 — must converge HERE since the read path never routes
|
|
552
576
|
// through reconcileAgentRun), giving an untrusted-pid worker ample time.
|
|
553
577
|
if (evidence.age_ms >= stale) {
|
|
578
|
+
if (fsActiveWithin(evidence, heartbeatStale)) {
|
|
579
|
+
return {
|
|
580
|
+
run_id: run.id, action: 'no_op',
|
|
581
|
+
reason: `no heartbeat but fs active ${Math.round((evidence.fs_activity_age_ms ?? 0) / 1000)}s ago - working, not silent`,
|
|
582
|
+
evidence, previous_status: run.status, current_status: run.status,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
554
585
|
return failRun('silent_termination_no_evidence');
|
|
555
586
|
}
|
|
556
587
|
emitUnverifiedEvent(run, evidence, actor, cwd);
|
|
@@ -562,7 +593,7 @@ export function reconcileDeadPidRunningAgentRunAtRead(runId, cwd, options = {})
|
|
|
562
593
|
}
|
|
563
594
|
export function sweepDeadPidRunningAgentRunsAtRead(cwd, options = {}) {
|
|
564
595
|
const now = options.nowMs ?? Date.now();
|
|
565
|
-
const minAgeMs = options.
|
|
596
|
+
const minAgeMs = options.deadPidSweepCandidateAgeMs ?? DEFAULT_DEAD_PID_READ_SWEEP_AGE_MS;
|
|
566
597
|
const cutoff = now - minAgeMs;
|
|
567
598
|
const limit = options.limit ?? DEFAULT_DEAD_PID_READ_SWEEP_LIMIT;
|
|
568
599
|
const candidates = listAgentRuns(cwd, { status: 'running' })
|
package/dist/core/agentruns.js
CHANGED
|
@@ -15,6 +15,7 @@ import { JsonStore } from './json-store.js';
|
|
|
15
15
|
import { appendAuditEntry } from './audit.js';
|
|
16
16
|
import { appendEvent } from './event-log.js';
|
|
17
17
|
import { createRuntimeEvent } from './events.js';
|
|
18
|
+
import { emitRegistryPostImage, registryFaultPoint } from './events/registry-post-image.js';
|
|
18
19
|
function agentRunsDir(cwd, mode = 'read') {
|
|
19
20
|
return resolveEntityDir('runs', cwd, mode);
|
|
20
21
|
}
|
|
@@ -46,7 +47,12 @@ export function saveAgentRun(run, cwd) {
|
|
|
46
47
|
getId: (item) => item.id,
|
|
47
48
|
sort: (a, b) => a.created_at.localeCompare(b.created_at),
|
|
48
49
|
});
|
|
49
|
-
|
|
50
|
+
const parsed = AgentRunSchema.parse(run);
|
|
51
|
+
// pln#568 (I2): journal the post-image BEFORE the projection write.
|
|
52
|
+
const created = !store.exists(parsed.id);
|
|
53
|
+
emitRegistryPostImage('agent_run', parsed, { created, agent: parsed.agent, agent_id: parsed.agent_id, session_id: parsed.session_id, cwd });
|
|
54
|
+
registryFaultPoint('after_registry_journal');
|
|
55
|
+
store.save(parsed);
|
|
50
56
|
});
|
|
51
57
|
}
|
|
52
58
|
export function loadAgentRun(id, cwd) {
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import os from 'node:os';
|
|
4
1
|
/**
|
|
5
2
|
* Detects the AI coding agent running in the current environment by inspecting
|
|
6
|
-
* environment variables
|
|
3
|
+
* PROCESS-SCOPED environment variables ONLY. Returns the first confident
|
|
7
4
|
* match, or undefined if no agent is detected.
|
|
8
5
|
*
|
|
6
|
+
* Identity hardening (pln#562 step 1): directory-presence fallbacks
|
|
7
|
+
* (~/.config/opencode, ~/.gemini/antigravity, ~/.openclaw, ~/.vibe, ~/.hermes)
|
|
8
|
+
* were removed — a config directory proves an agent is INSTALLED on the
|
|
9
|
+
* machine, not that it is the agent driving THIS process. Installed-ness is
|
|
10
|
+
* now answered exclusively by agent-inventory (buildAgentInventory). The
|
|
11
|
+
* inventory never mints identity; this function never consults the disk.
|
|
12
|
+
*
|
|
9
13
|
* Detection order (highest confidence first — agents with dedicated env vars
|
|
10
14
|
* are tested before agents detected via passive/ambient env vars):
|
|
11
15
|
* 1. BRAINCLAW_AGENT env var (explicit override)
|
|
@@ -14,21 +18,21 @@ import os from 'node:os';
|
|
|
14
18
|
* 4. Windsurf (WINDSURF_SESSION_ID — set by Windsurf itself)
|
|
15
19
|
* 5. Cline (CLINE_AGENT — set by Cline itself)
|
|
16
20
|
* 6. GitHub Copilot (GITHUB_COPILOT_PRODUCT — passive VS Code env, tested after active agents)
|
|
17
|
-
* 7. Codex CLI (
|
|
18
|
-
* 8. OpenCode (OPENCODE_*
|
|
19
|
-
* 9. Antigravity / Gemini CLI (ANTIGRAVITY_*
|
|
21
|
+
* 7. Codex CLI (CODEX_THREAD_ID / CODEX_CI / …)
|
|
22
|
+
* 8. OpenCode (OPENCODE_*)
|
|
23
|
+
* 9. Antigravity / Gemini CLI (ANTIGRAVITY_*)
|
|
20
24
|
* 10. Continue (CONTINUE_*)
|
|
21
25
|
* 11. Roo Code (ROO_*)
|
|
22
|
-
* 12.
|
|
23
|
-
* 13.
|
|
26
|
+
* 12. OpenClaw (OPENCLAW_*)
|
|
27
|
+
* 13. Mistral Vibe (VIBE_HOME)
|
|
28
|
+
* 14. Hermes (HERMES_*)
|
|
24
29
|
*/
|
|
25
|
-
export function detectAiAgent(env = process.env
|
|
30
|
+
export function detectAiAgent(env = process.env) {
|
|
26
31
|
// Explicit override
|
|
27
32
|
if (env.BRAINCLAW_AGENT?.trim()) {
|
|
28
33
|
return {
|
|
29
34
|
name: env.BRAINCLAW_AGENT.trim(),
|
|
30
35
|
kind: 'agent',
|
|
31
|
-
trust_level: 'trusted',
|
|
32
36
|
detection_source: 'BRAINCLAW_AGENT env var',
|
|
33
37
|
};
|
|
34
38
|
}
|
|
@@ -44,7 +48,6 @@ export function detectAiAgent(env = process.env, homeDir = os.homedir()) {
|
|
|
44
48
|
return {
|
|
45
49
|
name: 'claude-code',
|
|
46
50
|
kind: 'agent',
|
|
47
|
-
trust_level: 'trusted',
|
|
48
51
|
detection_source: source,
|
|
49
52
|
};
|
|
50
53
|
}
|
|
@@ -53,7 +56,6 @@ export function detectAiAgent(env = process.env, homeDir = os.homedir()) {
|
|
|
53
56
|
return {
|
|
54
57
|
name: 'cursor',
|
|
55
58
|
kind: 'agent',
|
|
56
|
-
trust_level: 'trusted',
|
|
57
59
|
detection_source: 'CURSOR_* env var',
|
|
58
60
|
};
|
|
59
61
|
}
|
|
@@ -62,7 +64,6 @@ export function detectAiAgent(env = process.env, homeDir = os.homedir()) {
|
|
|
62
64
|
return {
|
|
63
65
|
name: 'windsurf',
|
|
64
66
|
kind: 'agent',
|
|
65
|
-
trust_level: 'trusted',
|
|
66
67
|
detection_source: 'WINDSURF_* env var',
|
|
67
68
|
};
|
|
68
69
|
}
|
|
@@ -71,7 +72,6 @@ export function detectAiAgent(env = process.env, homeDir = os.homedir()) {
|
|
|
71
72
|
return {
|
|
72
73
|
name: 'cline',
|
|
73
74
|
kind: 'agent',
|
|
74
|
-
trust_level: 'trusted',
|
|
75
75
|
detection_source: 'CLINE_* env var',
|
|
76
76
|
};
|
|
77
77
|
}
|
|
@@ -85,7 +85,6 @@ export function detectAiAgent(env = process.env, homeDir = os.homedir()) {
|
|
|
85
85
|
return {
|
|
86
86
|
name: 'github-copilot',
|
|
87
87
|
kind: 'agent',
|
|
88
|
-
trust_level: 'trusted',
|
|
89
88
|
detection_source: env.GITHUB_COPILOT_PRODUCT ? 'GITHUB_COPILOT_PRODUCT env var' : 'GITHUB_COPILOT_TOKEN env var',
|
|
90
89
|
};
|
|
91
90
|
}
|
|
@@ -101,26 +100,23 @@ export function detectAiAgent(env = process.env, homeDir = os.homedir()) {
|
|
|
101
100
|
return {
|
|
102
101
|
name: 'codex',
|
|
103
102
|
kind: 'agent',
|
|
104
|
-
trust_level: 'trusted',
|
|
105
103
|
detection_source: source,
|
|
106
104
|
};
|
|
107
105
|
}
|
|
108
106
|
// OpenCode
|
|
109
|
-
if (env.OPENCODE_SESSION_ID || env.OPENCODE_AGENT
|
|
107
|
+
if (env.OPENCODE_SESSION_ID || env.OPENCODE_AGENT) {
|
|
110
108
|
return {
|
|
111
109
|
name: 'opencode',
|
|
112
110
|
kind: 'agent',
|
|
113
|
-
|
|
114
|
-
detection_source: env.OPENCODE_SESSION_ID || env.OPENCODE_AGENT ? 'OPENCODE_* env var' : '~/.config/opencode directory',
|
|
111
|
+
detection_source: 'OPENCODE_* env var',
|
|
115
112
|
};
|
|
116
113
|
}
|
|
117
114
|
// Antigravity (Google Gemini CLI)
|
|
118
|
-
if (env.ANTIGRAVITY_SESSION_ID || env.ANTIGRAVITY_AGENT
|
|
115
|
+
if (env.ANTIGRAVITY_SESSION_ID || env.ANTIGRAVITY_AGENT) {
|
|
119
116
|
return {
|
|
120
117
|
name: 'antigravity',
|
|
121
118
|
kind: 'agent',
|
|
122
|
-
|
|
123
|
-
detection_source: env.ANTIGRAVITY_SESSION_ID || env.ANTIGRAVITY_AGENT ? 'ANTIGRAVITY_* env var' : '~/.gemini/antigravity directory',
|
|
119
|
+
detection_source: 'ANTIGRAVITY_* env var',
|
|
124
120
|
};
|
|
125
121
|
}
|
|
126
122
|
// Continue.dev
|
|
@@ -128,7 +124,6 @@ export function detectAiAgent(env = process.env, homeDir = os.homedir()) {
|
|
|
128
124
|
return {
|
|
129
125
|
name: 'continue',
|
|
130
126
|
kind: 'agent',
|
|
131
|
-
trust_level: 'trusted',
|
|
132
127
|
detection_source: 'CONTINUE_* env var',
|
|
133
128
|
};
|
|
134
129
|
}
|
|
@@ -137,44 +132,36 @@ export function detectAiAgent(env = process.env, homeDir = os.homedir()) {
|
|
|
137
132
|
return {
|
|
138
133
|
name: 'roo',
|
|
139
134
|
kind: 'agent',
|
|
140
|
-
trust_level: 'trusted',
|
|
141
135
|
detection_source: 'ROO_* env var',
|
|
142
136
|
};
|
|
143
137
|
}
|
|
144
|
-
// OpenClaw
|
|
145
|
-
if (env.OPENCLAW_SESSION_ID || env.OPENCLAW_AGENT
|
|
138
|
+
// OpenClaw
|
|
139
|
+
if (env.OPENCLAW_SESSION_ID || env.OPENCLAW_AGENT) {
|
|
146
140
|
return {
|
|
147
141
|
name: 'openclaw',
|
|
148
142
|
kind: 'agent',
|
|
149
|
-
|
|
150
|
-
detection_source: env.OPENCLAW_SESSION_ID || env.OPENCLAW_AGENT ? 'OPENCLAW_* env var' : '~/.openclaw directory',
|
|
143
|
+
detection_source: 'OPENCLAW_* env var',
|
|
151
144
|
};
|
|
152
145
|
}
|
|
153
|
-
// Mistral Vibe — no dedicated session env var documented (per pln#489
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
if (env.VIBE_HOME || fs.existsSync(path.join(homeDir, '.vibe'))) {
|
|
146
|
+
// Mistral Vibe — no dedicated session env var documented (per pln#489
|
|
147
|
+
// research). VIBE_HOME is the only process-scoped marker; the ~/.vibe
|
|
148
|
+
// directory check moved to agent-inventory.
|
|
149
|
+
if (env.VIBE_HOME) {
|
|
158
150
|
return {
|
|
159
151
|
name: 'mistral-vibe',
|
|
160
152
|
kind: 'agent',
|
|
161
|
-
|
|
162
|
-
detection_source: env.VIBE_HOME ? 'VIBE_HOME env var' : '~/.vibe directory',
|
|
153
|
+
detection_source: 'VIBE_HOME env var',
|
|
163
154
|
};
|
|
164
155
|
}
|
|
165
|
-
// Hermes Agent —
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
if (env.HERMES_SESSION_ID || env.HERMES_AGENT || env.HERMES_HOME || fs.existsSync(path.join(homeDir, '.hermes'))) {
|
|
156
|
+
// Hermes Agent — detect after editor/CLI agents with stronger session env
|
|
157
|
+
// vars to avoid stealing mixed shells where Hermes is merely installed.
|
|
158
|
+
if (env.HERMES_SESSION_ID || env.HERMES_AGENT || env.HERMES_HOME) {
|
|
169
159
|
return {
|
|
170
160
|
name: 'hermes',
|
|
171
161
|
kind: 'autonomous',
|
|
172
|
-
trust_level: 'trusted',
|
|
173
162
|
detection_source: env.HERMES_SESSION_ID || env.HERMES_AGENT
|
|
174
163
|
? 'HERMES_* env var'
|
|
175
|
-
: env
|
|
176
|
-
? 'HERMES_HOME env var'
|
|
177
|
-
: '~/.hermes directory',
|
|
164
|
+
: 'HERMES_HOME env var',
|
|
178
165
|
};
|
|
179
166
|
}
|
|
180
167
|
return undefined;
|