brainclaw 0.24.0 → 0.27.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.
@@ -9,7 +9,7 @@ import { buildContext, renderContextMarkdown, renderContextPromptTemplate } from
9
9
  import { buildExecutionContext, renderExecutionContextSummary } from '../core/execution-context.js';
10
10
  import { checkBrainclawInstallableUpdate, renderBrainclawInstallableUpdateNotice } from '../core/brainclaw-version.js';
11
11
  import { loadConfig } from '../core/config.js';
12
- import { loadState, persistState } from '../core/state.js';
12
+ import { loadState, persistState, saveState } from '../core/state.js';
13
13
  import { memoryExists } from '../core/io.js';
14
14
  import { generateCandidateIdWithLabel, listArchivedCandidates, listCandidates, saveCandidate } from '../core/candidates.js';
15
15
  import { generateClaimId, listClaims, loadClaim, saveClaim } from '../core/claims.js';
@@ -18,8 +18,8 @@ import { acceptCandidate } from './accept.js';
18
18
  import { rejectCandidate } from './reject.js';
19
19
  import { startSession } from './session-start.js';
20
20
  import { endSession } from './session-end.js';
21
- import { agentCanWriteDirect, AgentIdentityResolutionError, AgentTrustError, listAgentIdentities, requireMinimumTrustLevel, requireRegisteredAgentIdentity, resolveAgentScope, resolveCurrentAgentIdentity, resolveCurrentAgentName, } from '../core/agent-registry.js';
22
- import { appendAuditEntry } from '../core/audit.js';
21
+ import { agentCanWriteDirect, AgentIdentityResolutionError, AgentTrustError, listAgentIdentities, requireMinimumTrustLevel, requireRegisteredAgentIdentity, resolveAgentScope, resolveCurrentAgentIdentity, resolveCurrentAgentName, resolveCurrentModel, } from '../core/agent-registry.js';
22
+ import { appendAuditEntry, readAuditLog } from '../core/audit.js';
23
23
  import { nowISO, generateIdWithLabel, generateId } from '../core/ids.js';
24
24
  import { inferProjectFromTarget, loadInstructions, resolveInstructions } from '../core/instructions.js';
25
25
  import { buildReputationSnapshot, toPublicReputationSummary } from '../core/reputation.js';
@@ -27,6 +27,9 @@ import { search } from '../core/search.js';
27
27
  import { buildOperationalIdentity } from '../core/identity.js';
28
28
  import { validateMcpInput, validateMcpField } from '../core/input-validation.js';
29
29
  import { buildEstimationReport } from './estimation-report.js';
30
+ import { runDoctor } from './doctor.js';
31
+ import { buildProjectDiscovery, saveDiscoveryProfile, loadDiscoveryProfile, renderDiscoverySummary } from '../core/project-discovery.js';
32
+ import { listCapabilities, listTools as listRegistryTools, createCapability, createTool as createRegistryTool } from '../core/registries.js';
30
33
  import { detectAiAgent } from '../core/ai-agent-detection.js';
31
34
  import { checkGitPresence, scanGitRepos, parseRoots, parseRepoSelection, parseAgentSelection, runGlobalInstall, initReposAndConfigureAgents, readSetupState, ALL_KNOWN_AGENTS, } from './setup.js';
32
35
  import { resolveEffectiveCwd, resolveTargetStore, resolveStoreChain } from '../core/store-resolution.js';
@@ -156,6 +159,10 @@ export const MCP_READ_TOOLS = [
156
159
  type: { type: 'string', description: 'Filter by plan type.' },
157
160
  assignee: { type: 'string', description: 'Filter by assignee name.' },
158
161
  project: { type: 'string', description: 'Filter by project namespace.' },
162
+ id: { type: 'string', description: 'Get a single plan by ID (exact match).' },
163
+ limit: { type: 'number', description: 'Maximum number of plans to return (default: 20).' },
164
+ offset: { type: 'number', description: 'Number of plans to skip (for pagination).' },
165
+ compact: { type: 'boolean', description: 'Return only key fields (id, short_label, text, status, priority) to reduce output size.' },
159
166
  },
160
167
  },
161
168
  },
@@ -244,6 +251,62 @@ export const MCP_READ_TOOLS = [
244
251
  required: ['query'],
245
252
  },
246
253
  },
254
+ {
255
+ name: 'bclaw_doctor',
256
+ description: 'Run health checks on the brainclaw memory store. Returns structured check results with ok/warn/error status and metrics.',
257
+ inputSchema: {
258
+ type: 'object',
259
+ properties: {
260
+ migrationCheck: { type: 'boolean', description: 'Include detailed schema migration status.' },
261
+ },
262
+ },
263
+ },
264
+ {
265
+ name: 'bclaw_history',
266
+ description: 'Show full mutation history of a memory item from the audit log.',
267
+ inputSchema: {
268
+ type: 'object',
269
+ properties: {
270
+ id: { type: 'string', description: 'Item ID to retrieve history for.' },
271
+ },
272
+ required: ['id'],
273
+ },
274
+ },
275
+ {
276
+ name: 'bclaw_audit',
277
+ description: 'View the append-only audit log of all memory mutations.',
278
+ inputSchema: {
279
+ type: 'object',
280
+ properties: {
281
+ since: { type: 'string', description: 'Show entries since this ISO date.' },
282
+ actor: { type: 'string', description: 'Filter by actor name or agent ID.' },
283
+ action: { type: 'string', description: 'Filter by action type (create, accept, reject, etc.).' },
284
+ limit: { type: 'number', description: 'Show last N entries (default 20).' },
285
+ },
286
+ },
287
+ },
288
+ {
289
+ name: 'bclaw_get_discovery',
290
+ description: 'Scan workspace for MCP configs, instruction files, skills, hooks, and agent integrations. Returns a structured discovery profile. Saves result to .brainclaw/discovery/ by default.',
291
+ inputSchema: {
292
+ type: 'object',
293
+ properties: {
294
+ refresh: { type: 'boolean', description: 'Force a fresh scan even if a cached profile exists (default: true).' },
295
+ noSave: { type: 'boolean', description: 'Do not persist the discovery profile.' },
296
+ },
297
+ },
298
+ },
299
+ {
300
+ name: 'bclaw_conflict_check',
301
+ description: 'Check for claim conflicts between the current agent and other agents. Returns overlapping scopes.',
302
+ inputSchema: {
303
+ type: 'object',
304
+ properties: {
305
+ agent: { type: 'string', description: 'Agent name to check conflicts for (default: current agent).' },
306
+ agentId: { type: 'string', description: 'Registered agent id.' },
307
+ },
308
+ },
309
+ },
247
310
  ];
248
311
  const MCP_WRITE_TOOLS = [
249
312
  {
@@ -295,6 +358,8 @@ const MCP_WRITE_TOOLS = [
295
358
  outcome: { type: 'string', description: 'Outcome for decisions: approved, rejected, deferred, pending.' },
296
359
  severity: { type: 'string', description: 'Severity for traps: low, medium, high.' },
297
360
  planId: { type: 'string', description: 'Optional plan item ID this decision or trap relates to.' },
361
+ scope: { type: 'string', description: 'Memory scope: project (default), machine, or user. Machine-scoped items apply to all projects on this machine.' },
362
+ store: { type: 'string', description: 'Target store level: local (default), repo, workspace, user. Use "user" to write to ~/.brainclaw/ (visible across all projects).' },
298
363
  },
299
364
  required: ['text', 'type'],
300
365
  },
@@ -480,6 +545,52 @@ const MCP_WRITE_TOOLS = [
480
545
  required: ['id', 'type'],
481
546
  },
482
547
  },
548
+ {
549
+ name: 'bclaw_add_capability',
550
+ description: 'Register a new project capability. Requires contributor trust level or above.',
551
+ inputSchema: {
552
+ type: 'object',
553
+ properties: {
554
+ name: { type: 'string', description: 'Capability name.' },
555
+ description: { type: 'string', description: 'Capability description.' },
556
+ tags: { type: 'array', items: { type: 'string' }, description: 'Additional tags.' },
557
+ agent: { type: 'string', description: 'Agent name.' },
558
+ agentId: { type: 'string', description: 'Registered agent id.' },
559
+ },
560
+ required: ['name', 'description'],
561
+ },
562
+ },
563
+ {
564
+ name: 'bclaw_add_tool',
565
+ description: 'Register a new project tool. Requires contributor trust level or above.',
566
+ inputSchema: {
567
+ type: 'object',
568
+ properties: {
569
+ name: { type: 'string', description: 'Tool name.' },
570
+ description: { type: 'string', description: 'Tool description.' },
571
+ type: { type: 'string', description: 'Tool type: workflow, validator, generator, utility, explorer (default: utility).' },
572
+ tags: { type: 'array', items: { type: 'string' }, description: 'Additional tags.' },
573
+ agent: { type: 'string', description: 'Agent name.' },
574
+ agentId: { type: 'string', description: 'Registered agent id.' },
575
+ },
576
+ required: ['name', 'description'],
577
+ },
578
+ },
579
+ {
580
+ name: 'bclaw_update_handoff',
581
+ description: 'Update the status or recipient of an open handoff. Requires contributor trust level or above.',
582
+ inputSchema: {
583
+ type: 'object',
584
+ properties: {
585
+ id: { type: 'string', description: 'Handoff ID to update.' },
586
+ status: { type: 'string', description: 'New status: open, closed.' },
587
+ to: { type: 'string', description: 'New recipient agent name.' },
588
+ agent: { type: 'string', description: 'Agent name.' },
589
+ agentId: { type: 'string', description: 'Registered agent id.' },
590
+ },
591
+ required: ['id'],
592
+ },
593
+ },
483
594
  ];
484
595
  const ALL_TOOLS = [...MCP_READ_TOOLS, ...MCP_WRITE_TOOLS];
485
596
  class McpProtocolError extends Error {
@@ -1036,10 +1147,9 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1036
1147
  refreshBootstrap: args.refreshBootstrap,
1037
1148
  cwd,
1038
1149
  });
1039
- // Load available capabilities and tools
1040
- const state = loadState(cwd);
1041
- const capabilities = state.recent_decisions.filter((d) => d.tags.includes('capability'));
1042
- const tools = state.recent_decisions.filter((d) => d.tags.includes('tool'));
1150
+ // Load available capabilities and tools from dedicated registries
1151
+ const capabilities = listCapabilities(cwd);
1152
+ const tools = listRegistryTools(cwd);
1043
1153
  const format = normaliseFormat(args.format);
1044
1154
  const content = renderContextForMcp(result, format, {
1045
1155
  explain: args.explain,
@@ -1052,8 +1162,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1052
1162
  if (capabilities.length > 0) {
1053
1163
  suggestions.push(`\n## Available Capabilities (${capabilities.length})`);
1054
1164
  capabilities.slice(0, 5).forEach((cap) => {
1055
- const category = cap.tags.find((t) => t !== 'capability') || 'general';
1056
- suggestions.push(`- [${cap.id}] ${cap.text.split('\n')[0]} (${category})`);
1165
+ suggestions.push(`- [${cap.id}] ${cap.name} (${cap.category})`);
1057
1166
  });
1058
1167
  if (capabilities.length > 5) {
1059
1168
  suggestions.push(`- ... and ${capabilities.length - 5} more`);
@@ -1062,8 +1171,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1062
1171
  if (tools.length > 0) {
1063
1172
  suggestions.push(`\n## Available Tools (${tools.length})`);
1064
1173
  tools.slice(0, 5).forEach((tool) => {
1065
- const type = tool.tags.find((t) => t !== 'tool') || 'utility';
1066
- suggestions.push(`- [${tool.id}] ${tool.text.split('\n')[0]} (${type})`);
1174
+ suggestions.push(`- [${tool.id}] ${tool.name} (${tool.type})`);
1067
1175
  });
1068
1176
  if (tools.length > 5) {
1069
1177
  suggestions.push(`- ... and ${tools.length - 5} more`);
@@ -1082,13 +1190,13 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1082
1190
  ...result,
1083
1191
  available_capabilities: capabilities.map((cap) => ({
1084
1192
  id: cap.id,
1085
- name: cap.text.split('\n')[0],
1086
- category: cap.tags.find((t) => t !== 'capability') || 'general',
1193
+ name: cap.name,
1194
+ category: cap.category,
1087
1195
  })),
1088
1196
  available_tools: tools.map((tool) => ({
1089
1197
  id: tool.id,
1090
- name: tool.text.split('\n')[0],
1091
- type: tool.tags.find((t) => t !== 'tool') || 'utility',
1198
+ name: tool.name,
1199
+ type: tool.type,
1092
1200
  })),
1093
1201
  ...(notifications ? { pending_notifications: notifications, unseen_event_count: unseenEvents.length } : {}),
1094
1202
  },
@@ -1265,6 +1373,12 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1265
1373
  for (const instruction of board.resolved_instructions.slice(0, 10)) {
1266
1374
  lines.push(`- [${instruction.id}] <${instruction.layer}${instruction.scope ? `:${instruction.scope}` : ''}> ${instruction.text}`);
1267
1375
  }
1376
+ if (board.other_agents && board.other_agents.length > 0) {
1377
+ lines.push(`Other agents: ${board.other_agents.length}`);
1378
+ for (const other of board.other_agents) {
1379
+ lines.push(`- ${other.name}: ${other.claim_count} claim(s) on ${other.scopes.join(', ')}`);
1380
+ }
1381
+ }
1268
1382
  return {
1269
1383
  content: [{ type: 'text', text: lines.join('\n') }],
1270
1384
  structuredContent: { ...board },
@@ -1308,6 +1422,18 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1308
1422
  }
1309
1423
  if (name === 'bclaw_list_plans') {
1310
1424
  let plans = loadState(cwd).plan_items;
1425
+ // Direct lookup by ID
1426
+ if (args.id) {
1427
+ const plan = plans.find((p) => p.id === String(args.id) || p.short_label === String(args.id));
1428
+ if (!plan) {
1429
+ return { content: [{ type: 'text', text: `Plan '${args.id}' not found.` }], structuredContent: { total: 0, plans: [] } };
1430
+ }
1431
+ return {
1432
+ content: [{ type: 'text', text: `[${plan.id}] ${plan.text} (${plan.status}, ${plan.priority})` }],
1433
+ structuredContent: { total: 1, plans: [plan] },
1434
+ };
1435
+ }
1436
+ // Filters
1311
1437
  if (!args.all) {
1312
1438
  plans = plans.filter((plan) => plan.status !== 'done' && plan.status !== 'dropped');
1313
1439
  }
@@ -1325,11 +1451,16 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1325
1451
  const project = String(args.project).toLowerCase();
1326
1452
  plans = plans.filter((plan) => plan.project?.toLowerCase() === project);
1327
1453
  }
1328
- const lines = plans.length === 0
1454
+ const totalFiltered = plans.length;
1455
+ // Pagination
1456
+ const offset = Math.max(0, Number(args.offset) || 0);
1457
+ const limit = Math.max(1, Number(args.limit) || 20);
1458
+ const paginated = plans.slice(offset, offset + limit);
1459
+ const lines = paginated.length === 0
1329
1460
  ? ['No plan items found.']
1330
1461
  : [
1331
- `${plans.length} plan item(s):`,
1332
- ...plans.map((plan) => {
1462
+ `${totalFiltered} plan(s)${totalFiltered > paginated.length ? ` (showing ${offset + 1}-${offset + paginated.length})` : ''}:`,
1463
+ ...paginated.map((plan) => {
1333
1464
  const meta = [plan.type ?? 'feat', plan.status, plan.priority];
1334
1465
  if (plan.assignee)
1335
1466
  meta.push(`assignee ${plan.assignee}`);
@@ -1341,9 +1472,15 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1341
1472
  return `[${plan.id}] ${plan.text} (${meta.join(' Ā· ')})${tags}`;
1342
1473
  }),
1343
1474
  ];
1475
+ // Compact mode: strip heavy fields
1476
+ const outputPlans = args.compact
1477
+ ? paginated.map(({ id, short_label, text, status, priority, tags, assignee, type }) => ({
1478
+ id, short_label, text, status, priority, tags, assignee, type,
1479
+ }))
1480
+ : paginated;
1344
1481
  return {
1345
1482
  content: [{ type: 'text', text: lines.join('\n') }],
1346
- structuredContent: { total: plans.length, plans },
1483
+ structuredContent: { total: totalFiltered, offset, limit, plans: outputPlans },
1347
1484
  };
1348
1485
  }
1349
1486
  if (name === 'bclaw_list_claims') {
@@ -1496,32 +1633,25 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1496
1633
  };
1497
1634
  }
1498
1635
  if (name === 'bclaw_get_capabilities') {
1499
- const state = loadState(cwd);
1500
- const capabilities = state.recent_decisions.filter((d) => d.tags.includes('capability'));
1501
- const filtered = capabilities.filter((cap) => {
1636
+ const allCapabilities = listCapabilities(cwd);
1637
+ const filtered = allCapabilities.filter((cap) => {
1502
1638
  const categoryFilter = args.category;
1503
1639
  const tagsFilter = args.tags;
1504
- if (categoryFilter) {
1505
- const capCategory = cap.tags.find((t) => t !== 'capability');
1506
- if (capCategory !== categoryFilter)
1507
- return false;
1508
- }
1640
+ if (categoryFilter && cap.category !== categoryFilter)
1641
+ return false;
1509
1642
  if (tagsFilter && tagsFilter.length > 0) {
1510
- const hasAllTags = tagsFilter.every((tag) => cap.tags.includes(tag));
1511
- if (!hasAllTags)
1643
+ if (!tagsFilter.every((tag) => cap.tags.includes(tag)))
1512
1644
  return false;
1513
1645
  }
1514
1646
  return true;
1515
1647
  });
1516
1648
  const lines = [`Capabilities (${filtered.length}):`];
1517
1649
  filtered.forEach((cap) => {
1518
- const category = cap.tags.find((t) => t !== 'capability') || 'general';
1519
- const otherTags = cap.tags.filter((t) => t !== 'capability' && t !== category);
1520
- lines.push(`\n[${cap.id}] ${cap.text.split('\n')[0]}`);
1521
- lines.push(` Category: ${category}`);
1650
+ lines.push(`\n[${cap.id}] ${cap.name}`);
1651
+ lines.push(` Category: ${cap.category}`);
1522
1652
  lines.push(` Author: ${cap.author}`);
1523
- if (otherTags.length > 0) {
1524
- lines.push(` Tags: ${otherTags.join(', ')}`);
1653
+ if (cap.tags.length > 0) {
1654
+ lines.push(` Tags: ${cap.tags.join(', ')}`);
1525
1655
  }
1526
1656
  });
1527
1657
  return {
@@ -1530,32 +1660,25 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1530
1660
  };
1531
1661
  }
1532
1662
  if (name === 'bclaw_list_tools') {
1533
- const state = loadState(cwd);
1534
- const tools = state.recent_decisions.filter((d) => d.tags.includes('tool'));
1535
- const filtered = tools.filter((tool) => {
1663
+ const allTools = listRegistryTools(cwd);
1664
+ const filtered = allTools.filter((tool) => {
1536
1665
  const typeFilter = args.type;
1537
1666
  const tagsFilter = args.tags;
1538
- if (typeFilter) {
1539
- const toolType = tool.tags.find((t) => t !== 'tool');
1540
- if (toolType !== typeFilter)
1541
- return false;
1542
- }
1667
+ if (typeFilter && tool.type !== typeFilter)
1668
+ return false;
1543
1669
  if (tagsFilter && tagsFilter.length > 0) {
1544
- const hasAllTags = tagsFilter.every((tag) => tool.tags.includes(tag));
1545
- if (!hasAllTags)
1670
+ if (!tagsFilter.every((tag) => tool.tags.includes(tag)))
1546
1671
  return false;
1547
1672
  }
1548
1673
  return true;
1549
1674
  });
1550
1675
  const lines = [`Tools (${filtered.length}):`];
1551
1676
  filtered.forEach((tool) => {
1552
- const type = tool.tags.find((t) => t !== 'tool') || 'utility';
1553
- const otherTags = tool.tags.filter((t) => t !== 'tool' && t !== type);
1554
- lines.push(`\n[${tool.id}] ${tool.text.split('\n')[0]}`);
1555
- lines.push(` Type: ${type}`);
1677
+ lines.push(`\n[${tool.id}] ${tool.name}`);
1678
+ lines.push(` Type: ${tool.type}`);
1556
1679
  lines.push(` Author: ${tool.author}`);
1557
- if (otherTags.length > 0) {
1558
- lines.push(` Tags: ${otherTags.join(', ')}`);
1680
+ if (tool.tags.length > 0) {
1681
+ lines.push(` Tags: ${tool.tags.join(', ')}`);
1559
1682
  }
1560
1683
  });
1561
1684
  return {
@@ -1568,39 +1691,152 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1568
1691
  if (!query) {
1569
1692
  throw new Error('Missing required argument: query');
1570
1693
  }
1571
- const state = loadState(cwd);
1572
- const tools = state.recent_decisions.filter((d) => d.tags.includes('tool'));
1573
- const filtered = tools.filter((tool) => {
1694
+ const allTools = listRegistryTools(cwd);
1695
+ const queryLower = query.toLowerCase();
1696
+ const filtered = allTools.filter((tool) => {
1574
1697
  const typeFilter = args.type;
1575
1698
  const tagsFilter = args.tags;
1576
- // Type filter
1577
- if (typeFilter) {
1578
- const toolType = tool.tags.find((t) => t !== 'tool');
1579
- if (toolType !== typeFilter)
1580
- return false;
1581
- }
1582
- // Tags filter (all must match)
1699
+ if (typeFilter && tool.type !== typeFilter)
1700
+ return false;
1583
1701
  if (tagsFilter && tagsFilter.length > 0) {
1584
- const hasAllTags = tagsFilter.every((tag) => tool.tags.includes(tag));
1585
- if (!hasAllTags)
1702
+ if (!tagsFilter.every((tag) => tool.tags.includes(tag)))
1586
1703
  return false;
1587
1704
  }
1588
- // Query search (name, description, tags)
1589
- const queryLower = query.toLowerCase();
1590
- return (tool.text.toLowerCase().includes(queryLower) ||
1705
+ return (tool.name.toLowerCase().includes(queryLower) ||
1706
+ tool.description.toLowerCase().includes(queryLower) ||
1591
1707
  tool.tags.some((tag) => tag.toLowerCase().includes(queryLower)));
1592
1708
  });
1593
1709
  const lines = [`Search results for '${query}' (${filtered.length} tool(s)):`];
1594
1710
  filtered.forEach((tool) => {
1595
- const type = tool.tags.find((t) => t !== 'tool') || 'utility';
1596
- lines.push(`\n[${tool.id}] ${tool.text.split('\n')[0]}`);
1597
- lines.push(` Type: ${type}`);
1711
+ lines.push(`\n[${tool.id}] ${tool.name}`);
1712
+ lines.push(` Type: ${tool.type}`);
1598
1713
  });
1599
1714
  return {
1600
1715
  content: [{ type: 'text', text: lines.join('\n') || 'No tools found.' }],
1601
1716
  structuredContent: { query, total: filtered.length, tools: filtered },
1602
1717
  };
1603
1718
  }
1719
+ if (name === 'bclaw_get_discovery') {
1720
+ const refresh = args.refresh !== false; // default: true
1721
+ const noSave = args.noSave;
1722
+ let profile;
1723
+ if (!refresh) {
1724
+ profile = loadDiscoveryProfile(cwd);
1725
+ }
1726
+ if (!profile) {
1727
+ profile = buildProjectDiscovery({ cwd });
1728
+ if (!noSave) {
1729
+ saveDiscoveryProfile(profile, cwd);
1730
+ }
1731
+ }
1732
+ return {
1733
+ content: [{ type: 'text', text: renderDiscoverySummary(profile) }],
1734
+ structuredContent: { ...profile, schema_version: SCHEMA_VERSION },
1735
+ };
1736
+ }
1737
+ if (name === 'bclaw_conflict_check') {
1738
+ const agentNameArg = args.agent;
1739
+ const agentIdArg = args.agentId;
1740
+ const currentAgentName = agentNameArg ?? resolveCurrentAgentName(cwd);
1741
+ const allClaimsForCheck = listClaims(cwd).filter((c) => c.status === 'active');
1742
+ const myClaimsForCheck = allClaimsForCheck.filter((c) => agentIdArg ? c.agent_id === agentIdArg : c.agent === currentAgentName);
1743
+ const otherClaimsForCheck = allClaimsForCheck.filter((c) => agentIdArg ? c.agent_id !== agentIdArg : c.agent !== currentAgentName);
1744
+ const conflicts = [];
1745
+ for (const mine of myClaimsForCheck) {
1746
+ const myScopes = mine.scope.replace(/\\/g, '/').split(/\s+/);
1747
+ for (const other of otherClaimsForCheck) {
1748
+ const otherScopes = other.scope.replace(/\\/g, '/').split(/\s+/);
1749
+ for (const ms of myScopes) {
1750
+ for (const os of otherScopes) {
1751
+ if (ms === os || ms.startsWith(os + '/') || os.startsWith(ms + '/')) {
1752
+ conflicts.push({
1753
+ my_claim: mine.id, my_scope: mine.scope,
1754
+ other_claim: other.id, other_agent: other.agent, other_scope: other.scope,
1755
+ reason: ms === os ? `exact: ${ms}` : `overlap: ${ms} ↔ ${os}`,
1756
+ });
1757
+ }
1758
+ }
1759
+ }
1760
+ }
1761
+ }
1762
+ const text = conflicts.length === 0
1763
+ ? `No claim conflicts for ${currentAgentName}.`
1764
+ : `${conflicts.length} conflict(s) found:\n${conflicts.map((c) => ` ${c.my_scope} ↔ ${c.other_agent}:${c.other_scope} (${c.reason})`).join('\n')}`;
1765
+ return {
1766
+ content: [{ type: 'text', text }],
1767
+ structuredContent: { agent: currentAgentName, conflicts, total: conflicts.length, schema_version: SCHEMA_VERSION },
1768
+ };
1769
+ }
1770
+ if (name === 'bclaw_doctor') {
1771
+ // Capture doctor JSON output by redirecting console.log
1772
+ const captured = [];
1773
+ const origLog = console.log;
1774
+ const origWarn = console.warn;
1775
+ const origError = console.error;
1776
+ console.log = (...a) => captured.push(a.join(' '));
1777
+ console.warn = (...a) => captured.push(a.join(' '));
1778
+ console.error = (...a) => captured.push(a.join(' '));
1779
+ try {
1780
+ runDoctor({ json: true, cwd, migrationCheck: args.migrationCheck });
1781
+ }
1782
+ finally {
1783
+ console.log = origLog;
1784
+ console.warn = origWarn;
1785
+ console.error = origError;
1786
+ }
1787
+ const jsonStr = captured.join('\n');
1788
+ let structured = {};
1789
+ try {
1790
+ structured = JSON.parse(jsonStr);
1791
+ }
1792
+ catch { /* non-JSON fallback */ }
1793
+ const ok = structured.ok;
1794
+ const checks = structured.checks ?? [];
1795
+ const errors = checks.filter(c => c.status === 'error');
1796
+ const warns = checks.filter(c => c.status === 'warn');
1797
+ const summary = ok
1798
+ ? `āœ” All ${checks.length} checks passed.`
1799
+ : `${errors.length} error(s), ${warns.length} warning(s) out of ${checks.length} checks.`;
1800
+ return {
1801
+ content: [{ type: 'text', text: summary }],
1802
+ structuredContent: { ...structured, schema_version: SCHEMA_VERSION },
1803
+ };
1804
+ }
1805
+ if (name === 'bclaw_history') {
1806
+ const id = String(args.id ?? '').trim();
1807
+ if (!id)
1808
+ throw new Error('Missing required argument: id');
1809
+ const entries = readAuditLog({ itemId: id }, cwd);
1810
+ const lines = [`History for ${id} — ${entries.length} event(s):`];
1811
+ for (const e of entries) {
1812
+ const reason = e.reason ? ` | ${e.reason}` : '';
1813
+ lines.push(` ${e.timestamp} [${e.actor}] ${e.action}${reason}`);
1814
+ }
1815
+ return {
1816
+ content: [{ type: 'text', text: lines.join('\n') }],
1817
+ structuredContent: { id, total: entries.length, entries, schema_version: SCHEMA_VERSION },
1818
+ };
1819
+ }
1820
+ if (name === 'bclaw_audit') {
1821
+ const limit = args.limit ?? 20;
1822
+ const entries = readAuditLog({
1823
+ since: args.since,
1824
+ actor: args.actor,
1825
+ action: args.action,
1826
+ }, cwd);
1827
+ const sliced = entries.slice(-limit);
1828
+ const lines = [`Audit log — showing ${sliced.length} of ${entries.length} entries:`];
1829
+ for (const e of sliced) {
1830
+ const itemInfo = e.item_id ? ` → ${e.item_id}` : '';
1831
+ const typeInfo = e.item_type ? ` (${e.item_type})` : '';
1832
+ const reason = e.reason ? ` | ${e.reason}` : '';
1833
+ lines.push(` ${e.timestamp} [${e.actor}] ${e.action}${itemInfo}${typeInfo}${reason}`);
1834
+ }
1835
+ return {
1836
+ content: [{ type: 'text', text: lines.join('\n') }],
1837
+ structuredContent: { total: entries.length, returned: sliced.length, entries: sliced, schema_version: SCHEMA_VERSION },
1838
+ };
1839
+ }
1604
1840
  throw new Error(`Unknown read tool: ${name}`);
1605
1841
  }
1606
1842
  export async function executeMcpToolCall(payload) {
@@ -1611,6 +1847,8 @@ export async function executeMcpToolCall(payload) {
1611
1847
  response: toolResponse(handleMcpReadToolCall(name, args, { cwd })),
1612
1848
  };
1613
1849
  }
1850
+ // Resolve model once for all write operations
1851
+ const currentModel = resolveCurrentModel(cwd);
1614
1852
  if (name === 'bclaw_setup') {
1615
1853
  const step = args.step;
1616
1854
  const choice = args.choice ?? '';
@@ -1794,6 +2032,7 @@ export async function executeMcpToolCall(payload) {
1794
2032
  autoReflect: args.autoReflect,
1795
2033
  cwd,
1796
2034
  sessionId: connectionSessionId,
2035
+ model: currentModel,
1797
2036
  }, false);
1798
2037
  return {
1799
2038
  response: toolResponse({
@@ -1829,6 +2068,9 @@ export async function executeMcpToolCall(payload) {
1829
2068
  const type = String(args.type ?? 'decision');
1830
2069
  const writeThrough = agentCanWriteDirect(identity.agent_id ?? resolvedIdentity.agent_id, cwd);
1831
2070
  const candidatePlanId = args.planId;
2071
+ const candidateScope = args.scope;
2072
+ const targetStore = args.store;
2073
+ const effectiveCwd = targetStore ? resolveTargetStore(cwd, targetStore) : cwd;
1832
2074
  const candidate = {
1833
2075
  id: candId.id,
1834
2076
  short_label: candId.short_label,
@@ -1845,7 +2087,9 @@ export async function executeMcpToolCall(payload) {
1845
2087
  severity: type === 'trap' ? (args.severity ?? 'medium') : undefined,
1846
2088
  category: type === 'constraint' ? args.category : undefined,
1847
2089
  outcome: type === 'decision' ? args.outcome : undefined,
2090
+ scope: candidateScope,
1848
2091
  plan_id: candidatePlanId,
2092
+ model: currentModel,
1849
2093
  star_count: 0,
1850
2094
  starred_by: [],
1851
2095
  usage_count: 0,
@@ -1854,22 +2098,25 @@ export async function executeMcpToolCall(payload) {
1854
2098
  const planPrompt = (type === 'decision' || type === 'trap') && !candidatePlanId
1855
2099
  ? `\nšŸ’” Does this ${type} relate to an active plan item? If so, re-run with planId: 'pln_xxx' to link it.`
1856
2100
  : '';
2101
+ const storeLabel = targetStore && targetStore !== 'local' ? ` [store: ${targetStore}]` : '';
1857
2102
  if (writeThrough) {
1858
- saveCandidate(candidate, cwd);
1859
- const accepted = acceptCandidate(candId.id, resolvedIdentity.agent_name, cwd, resolvedIdentity.agent_id);
1860
- appendAuditEntry({ actor: resolvedIdentity.agent_name, actor_id: resolvedIdentity.agent_id, action: 'promote_direct', item_id: candId.id, item_type: type }, cwd);
2103
+ saveCandidate(candidate, effectiveCwd);
2104
+ const accepted = acceptCandidate(candId.id, resolvedIdentity.agent_name, effectiveCwd, resolvedIdentity.agent_id);
2105
+ appendAuditEntry({ actor: resolvedIdentity.agent_name, actor_id: resolvedIdentity.agent_id, action: 'promote_direct', item_id: candId.id, item_type: type }, effectiveCwd);
1861
2106
  return {
1862
2107
  response: toolResponse({
1863
- content: [{ type: 'text', text: `āœ” Direct write [${candId.short_label}] (trusted agent)${planPrompt}` }],
2108
+ content: [{ type: 'text', text: `āœ” Direct write [${candId.short_label}] (trusted agent)${storeLabel}${planPrompt}` }],
1864
2109
  candidate_id: candId.id,
1865
2110
  promoted_item_id: accepted.promoted_item_id,
1866
2111
  write_through: true,
2112
+ store: targetStore ?? 'local',
2113
+ scope: candidateScope,
1867
2114
  }),
1868
2115
  nextConnectionSessionId: explicitSessionIdFromEnv() ? undefined : identity.session_id,
1869
2116
  };
1870
2117
  }
1871
- saveCandidate(candidate, cwd);
1872
- appendAuditEntry({ actor: resolvedIdentity.agent_name, actor_id: resolvedIdentity.agent_id, action: 'create', item_id: candId.id, item_type: type }, cwd);
2118
+ saveCandidate(candidate, effectiveCwd);
2119
+ appendAuditEntry({ actor: resolvedIdentity.agent_name, actor_id: resolvedIdentity.agent_id, action: 'create', item_id: candId.id, item_type: type }, effectiveCwd);
1873
2120
  return {
1874
2121
  response: toolResponse({
1875
2122
  content: [{ type: 'text', text: `āœ” Candidate created [${candId.short_label}] (pending review)${planPrompt}` }],
@@ -1949,6 +2196,7 @@ export async function executeMcpToolCall(payload) {
1949
2196
  created_at: nowISO(),
1950
2197
  status: 'active',
1951
2198
  plan_id: args.planId,
2199
+ model: currentModel,
1952
2200
  }, claimCwd);
1953
2201
  appendAuditEntry({ actor: resolvedIdentity.agent_name, actor_id: resolvedIdentity.agent_id, action: 'claim', item_id: claimId, item_type: 'claim' }, claimCwd);
1954
2202
  const postClaimItems = getTriggeredItems('trigger:post-claim', claimCwd);
@@ -2436,6 +2684,100 @@ export async function executeMcpToolCall(payload) {
2436
2684
  }),
2437
2685
  };
2438
2686
  }
2687
+ if (name === 'bclaw_add_capability') {
2688
+ const capName = String(args.name ?? '').trim();
2689
+ const capDesc = String(args.description ?? '').trim();
2690
+ if (!capName || !capDesc) {
2691
+ return { response: createToolErrorResponse('validation_error', 'Missing required arguments: name and description') };
2692
+ }
2693
+ const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd);
2694
+ if (resolved.error) {
2695
+ return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
2696
+ }
2697
+ const resolvedIdentity = resolved.identity;
2698
+ const extraTags = Array.isArray(args.tags) ? args.tags : [];
2699
+ const cap = createCapability({
2700
+ name: capName,
2701
+ description: capDesc,
2702
+ tags: extraTags,
2703
+ author: resolvedIdentity.agent_name,
2704
+ authorId: resolvedIdentity.agent_id,
2705
+ model: currentModel,
2706
+ }, cwd);
2707
+ appendAuditEntry({ actor: resolvedIdentity.agent_name, actor_id: resolvedIdentity.agent_id, action: 'create', item_id: cap.id, item_type: 'capability', reason: `capability: ${capName}` }, cwd);
2708
+ return {
2709
+ response: toolResponse({
2710
+ content: [{ type: 'text', text: `āœ” Capability registered: [${cap.id}] ${capName}` }],
2711
+ id: cap.id,
2712
+ name: capName,
2713
+ schema_version: SCHEMA_VERSION,
2714
+ }),
2715
+ };
2716
+ }
2717
+ if (name === 'bclaw_add_tool') {
2718
+ const toolName = String(args.name ?? '').trim();
2719
+ const toolDesc = String(args.description ?? '').trim();
2720
+ if (!toolName || !toolDesc) {
2721
+ return { response: createToolErrorResponse('validation_error', 'Missing required arguments: name and description') };
2722
+ }
2723
+ const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd);
2724
+ if (resolved.error) {
2725
+ return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
2726
+ }
2727
+ const resolvedIdentity = resolved.identity;
2728
+ const toolType = String(args.type ?? 'utility');
2729
+ const extraTags = Array.isArray(args.tags) ? args.tags : [];
2730
+ const tool = createRegistryTool({
2731
+ name: toolName,
2732
+ description: toolDesc,
2733
+ type: toolType,
2734
+ tags: extraTags,
2735
+ author: resolvedIdentity.agent_name,
2736
+ authorId: resolvedIdentity.agent_id,
2737
+ model: currentModel,
2738
+ }, cwd);
2739
+ appendAuditEntry({ actor: resolvedIdentity.agent_name, actor_id: resolvedIdentity.agent_id, action: 'create', item_id: tool.id, item_type: 'tool', reason: `tool: ${toolName}` }, cwd);
2740
+ return {
2741
+ response: toolResponse({
2742
+ content: [{ type: 'text', text: `āœ” Tool registered: [${tool.id}] ${toolName} (${toolType})` }],
2743
+ id: tool.id,
2744
+ name: toolName,
2745
+ type: toolType,
2746
+ schema_version: SCHEMA_VERSION,
2747
+ }),
2748
+ };
2749
+ }
2750
+ if (name === 'bclaw_update_handoff') {
2751
+ const handoffId = String(args.id ?? '').trim();
2752
+ if (!handoffId) {
2753
+ return { response: createToolErrorResponse('validation_error', 'Missing required argument: id') };
2754
+ }
2755
+ const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd);
2756
+ if (resolved.error) {
2757
+ return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
2758
+ }
2759
+ const resolvedIdentity = resolved.identity;
2760
+ const state = loadState(cwd);
2761
+ const handoff = state.open_handoffs.find((h) => h.id === handoffId);
2762
+ if (!handoff) {
2763
+ return { response: createToolErrorResponse('not_found', `Handoff not found: ${handoffId}`) };
2764
+ }
2765
+ if (args.status)
2766
+ handoff.status = args.status;
2767
+ if (args.to)
2768
+ handoff.to = String(args.to);
2769
+ saveState(state, cwd);
2770
+ appendAuditEntry({ actor: resolvedIdentity.agent_name, actor_id: resolvedIdentity.agent_id, action: 'update', item_id: handoffId, item_type: 'handoff' }, cwd);
2771
+ return {
2772
+ response: toolResponse({
2773
+ content: [{ type: 'text', text: `āœ” Handoff updated: [${handoffId}] ${handoff.from} → ${handoff.to} (${handoff.status})` }],
2774
+ handoff_id: handoffId,
2775
+ status: handoff.status,
2776
+ to: handoff.to,
2777
+ schema_version: SCHEMA_VERSION,
2778
+ }),
2779
+ };
2780
+ }
2439
2781
  return {
2440
2782
  response: createToolErrorResponse('unknown_tool', `Unknown tool: ${name}`),
2441
2783
  };