brainclaw 0.25.3 → 0.28.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';
@@ -19,7 +19,7 @@ import { rejectCandidate } from './reject.js';
19
19
  import { startSession } from './session-start.js';
20
20
  import { endSession } from './session-end.js';
21
21
  import { agentCanWriteDirect, AgentIdentityResolutionError, AgentTrustError, listAgentIdentities, requireMinimumTrustLevel, requireRegisteredAgentIdentity, resolveAgentScope, resolveCurrentAgentIdentity, resolveCurrentAgentName, resolveCurrentModel, } from '../core/agent-registry.js';
22
- import { appendAuditEntry } from '../core/audit.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';
@@ -248,6 +251,62 @@ export const MCP_READ_TOOLS = [
248
251
  required: ['query'],
249
252
  },
250
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
+ },
251
310
  ];
252
311
  const MCP_WRITE_TOOLS = [
253
312
  {
@@ -299,6 +358,8 @@ const MCP_WRITE_TOOLS = [
299
358
  outcome: { type: 'string', description: 'Outcome for decisions: approved, rejected, deferred, pending.' },
300
359
  severity: { type: 'string', description: 'Severity for traps: low, medium, high.' },
301
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).' },
302
363
  },
303
364
  required: ['text', 'type'],
304
365
  },
@@ -484,6 +545,52 @@ const MCP_WRITE_TOOLS = [
484
545
  required: ['id', 'type'],
485
546
  },
486
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
+ },
487
594
  ];
488
595
  const ALL_TOOLS = [...MCP_READ_TOOLS, ...MCP_WRITE_TOOLS];
489
596
  class McpProtocolError extends Error {
@@ -1040,10 +1147,9 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1040
1147
  refreshBootstrap: args.refreshBootstrap,
1041
1148
  cwd,
1042
1149
  });
1043
- // Load available capabilities and tools
1044
- const state = loadState(cwd);
1045
- const capabilities = state.recent_decisions.filter((d) => d.tags.includes('capability'));
1046
- 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);
1047
1153
  const format = normaliseFormat(args.format);
1048
1154
  const content = renderContextForMcp(result, format, {
1049
1155
  explain: args.explain,
@@ -1056,8 +1162,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1056
1162
  if (capabilities.length > 0) {
1057
1163
  suggestions.push(`\n## Available Capabilities (${capabilities.length})`);
1058
1164
  capabilities.slice(0, 5).forEach((cap) => {
1059
- const category = cap.tags.find((t) => t !== 'capability') || 'general';
1060
- suggestions.push(`- [${cap.id}] ${cap.text.split('\n')[0]} (${category})`);
1165
+ suggestions.push(`- [${cap.id}] ${cap.name} (${cap.category})`);
1061
1166
  });
1062
1167
  if (capabilities.length > 5) {
1063
1168
  suggestions.push(`- ... and ${capabilities.length - 5} more`);
@@ -1066,8 +1171,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1066
1171
  if (tools.length > 0) {
1067
1172
  suggestions.push(`\n## Available Tools (${tools.length})`);
1068
1173
  tools.slice(0, 5).forEach((tool) => {
1069
- const type = tool.tags.find((t) => t !== 'tool') || 'utility';
1070
- suggestions.push(`- [${tool.id}] ${tool.text.split('\n')[0]} (${type})`);
1174
+ suggestions.push(`- [${tool.id}] ${tool.name} (${tool.type})`);
1071
1175
  });
1072
1176
  if (tools.length > 5) {
1073
1177
  suggestions.push(`- ... and ${tools.length - 5} more`);
@@ -1086,13 +1190,13 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1086
1190
  ...result,
1087
1191
  available_capabilities: capabilities.map((cap) => ({
1088
1192
  id: cap.id,
1089
- name: cap.text.split('\n')[0],
1090
- category: cap.tags.find((t) => t !== 'capability') || 'general',
1193
+ name: cap.name,
1194
+ category: cap.category,
1091
1195
  })),
1092
1196
  available_tools: tools.map((tool) => ({
1093
1197
  id: tool.id,
1094
- name: tool.text.split('\n')[0],
1095
- type: tool.tags.find((t) => t !== 'tool') || 'utility',
1198
+ name: tool.name,
1199
+ type: tool.type,
1096
1200
  })),
1097
1201
  ...(notifications ? { pending_notifications: notifications, unseen_event_count: unseenEvents.length } : {}),
1098
1202
  },
@@ -1269,6 +1373,12 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1269
1373
  for (const instruction of board.resolved_instructions.slice(0, 10)) {
1270
1374
  lines.push(`- [${instruction.id}] <${instruction.layer}${instruction.scope ? `:${instruction.scope}` : ''}> ${instruction.text}`);
1271
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
+ }
1272
1382
  return {
1273
1383
  content: [{ type: 'text', text: lines.join('\n') }],
1274
1384
  structuredContent: { ...board },
@@ -1523,32 +1633,25 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1523
1633
  };
1524
1634
  }
1525
1635
  if (name === 'bclaw_get_capabilities') {
1526
- const state = loadState(cwd);
1527
- const capabilities = state.recent_decisions.filter((d) => d.tags.includes('capability'));
1528
- const filtered = capabilities.filter((cap) => {
1636
+ const allCapabilities = listCapabilities(cwd);
1637
+ const filtered = allCapabilities.filter((cap) => {
1529
1638
  const categoryFilter = args.category;
1530
1639
  const tagsFilter = args.tags;
1531
- if (categoryFilter) {
1532
- const capCategory = cap.tags.find((t) => t !== 'capability');
1533
- if (capCategory !== categoryFilter)
1534
- return false;
1535
- }
1640
+ if (categoryFilter && cap.category !== categoryFilter)
1641
+ return false;
1536
1642
  if (tagsFilter && tagsFilter.length > 0) {
1537
- const hasAllTags = tagsFilter.every((tag) => cap.tags.includes(tag));
1538
- if (!hasAllTags)
1643
+ if (!tagsFilter.every((tag) => cap.tags.includes(tag)))
1539
1644
  return false;
1540
1645
  }
1541
1646
  return true;
1542
1647
  });
1543
1648
  const lines = [`Capabilities (${filtered.length}):`];
1544
1649
  filtered.forEach((cap) => {
1545
- const category = cap.tags.find((t) => t !== 'capability') || 'general';
1546
- const otherTags = cap.tags.filter((t) => t !== 'capability' && t !== category);
1547
- lines.push(`\n[${cap.id}] ${cap.text.split('\n')[0]}`);
1548
- lines.push(` Category: ${category}`);
1650
+ lines.push(`\n[${cap.id}] ${cap.name}`);
1651
+ lines.push(` Category: ${cap.category}`);
1549
1652
  lines.push(` Author: ${cap.author}`);
1550
- if (otherTags.length > 0) {
1551
- lines.push(` Tags: ${otherTags.join(', ')}`);
1653
+ if (cap.tags.length > 0) {
1654
+ lines.push(` Tags: ${cap.tags.join(', ')}`);
1552
1655
  }
1553
1656
  });
1554
1657
  return {
@@ -1557,32 +1660,25 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1557
1660
  };
1558
1661
  }
1559
1662
  if (name === 'bclaw_list_tools') {
1560
- const state = loadState(cwd);
1561
- const tools = state.recent_decisions.filter((d) => d.tags.includes('tool'));
1562
- const filtered = tools.filter((tool) => {
1663
+ const allTools = listRegistryTools(cwd);
1664
+ const filtered = allTools.filter((tool) => {
1563
1665
  const typeFilter = args.type;
1564
1666
  const tagsFilter = args.tags;
1565
- if (typeFilter) {
1566
- const toolType = tool.tags.find((t) => t !== 'tool');
1567
- if (toolType !== typeFilter)
1568
- return false;
1569
- }
1667
+ if (typeFilter && tool.type !== typeFilter)
1668
+ return false;
1570
1669
  if (tagsFilter && tagsFilter.length > 0) {
1571
- const hasAllTags = tagsFilter.every((tag) => tool.tags.includes(tag));
1572
- if (!hasAllTags)
1670
+ if (!tagsFilter.every((tag) => tool.tags.includes(tag)))
1573
1671
  return false;
1574
1672
  }
1575
1673
  return true;
1576
1674
  });
1577
1675
  const lines = [`Tools (${filtered.length}):`];
1578
1676
  filtered.forEach((tool) => {
1579
- const type = tool.tags.find((t) => t !== 'tool') || 'utility';
1580
- const otherTags = tool.tags.filter((t) => t !== 'tool' && t !== type);
1581
- lines.push(`\n[${tool.id}] ${tool.text.split('\n')[0]}`);
1582
- lines.push(` Type: ${type}`);
1677
+ lines.push(`\n[${tool.id}] ${tool.name}`);
1678
+ lines.push(` Type: ${tool.type}`);
1583
1679
  lines.push(` Author: ${tool.author}`);
1584
- if (otherTags.length > 0) {
1585
- lines.push(` Tags: ${otherTags.join(', ')}`);
1680
+ if (tool.tags.length > 0) {
1681
+ lines.push(` Tags: ${tool.tags.join(', ')}`);
1586
1682
  }
1587
1683
  });
1588
1684
  return {
@@ -1595,39 +1691,152 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1595
1691
  if (!query) {
1596
1692
  throw new Error('Missing required argument: query');
1597
1693
  }
1598
- const state = loadState(cwd);
1599
- const tools = state.recent_decisions.filter((d) => d.tags.includes('tool'));
1600
- const filtered = tools.filter((tool) => {
1694
+ const allTools = listRegistryTools(cwd);
1695
+ const queryLower = query.toLowerCase();
1696
+ const filtered = allTools.filter((tool) => {
1601
1697
  const typeFilter = args.type;
1602
1698
  const tagsFilter = args.tags;
1603
- // Type filter
1604
- if (typeFilter) {
1605
- const toolType = tool.tags.find((t) => t !== 'tool');
1606
- if (toolType !== typeFilter)
1607
- return false;
1608
- }
1609
- // Tags filter (all must match)
1699
+ if (typeFilter && tool.type !== typeFilter)
1700
+ return false;
1610
1701
  if (tagsFilter && tagsFilter.length > 0) {
1611
- const hasAllTags = tagsFilter.every((tag) => tool.tags.includes(tag));
1612
- if (!hasAllTags)
1702
+ if (!tagsFilter.every((tag) => tool.tags.includes(tag)))
1613
1703
  return false;
1614
1704
  }
1615
- // Query search (name, description, tags)
1616
- const queryLower = query.toLowerCase();
1617
- return (tool.text.toLowerCase().includes(queryLower) ||
1705
+ return (tool.name.toLowerCase().includes(queryLower) ||
1706
+ tool.description.toLowerCase().includes(queryLower) ||
1618
1707
  tool.tags.some((tag) => tag.toLowerCase().includes(queryLower)));
1619
1708
  });
1620
1709
  const lines = [`Search results for '${query}' (${filtered.length} tool(s)):`];
1621
1710
  filtered.forEach((tool) => {
1622
- const type = tool.tags.find((t) => t !== 'tool') || 'utility';
1623
- lines.push(`\n[${tool.id}] ${tool.text.split('\n')[0]}`);
1624
- lines.push(` Type: ${type}`);
1711
+ lines.push(`\n[${tool.id}] ${tool.name}`);
1712
+ lines.push(` Type: ${tool.type}`);
1625
1713
  });
1626
1714
  return {
1627
1715
  content: [{ type: 'text', text: lines.join('\n') || 'No tools found.' }],
1628
1716
  structuredContent: { query, total: filtered.length, tools: filtered },
1629
1717
  };
1630
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
+ }
1631
1840
  throw new Error(`Unknown read tool: ${name}`);
1632
1841
  }
1633
1842
  export async function executeMcpToolCall(payload) {
@@ -1859,6 +2068,9 @@ export async function executeMcpToolCall(payload) {
1859
2068
  const type = String(args.type ?? 'decision');
1860
2069
  const writeThrough = agentCanWriteDirect(identity.agent_id ?? resolvedIdentity.agent_id, cwd);
1861
2070
  const candidatePlanId = args.planId;
2071
+ const candidateScope = args.scope;
2072
+ const targetStore = args.store;
2073
+ const effectiveCwd = targetStore ? resolveTargetStore(cwd, targetStore) : cwd;
1862
2074
  const candidate = {
1863
2075
  id: candId.id,
1864
2076
  short_label: candId.short_label,
@@ -1875,6 +2087,7 @@ export async function executeMcpToolCall(payload) {
1875
2087
  severity: type === 'trap' ? (args.severity ?? 'medium') : undefined,
1876
2088
  category: type === 'constraint' ? args.category : undefined,
1877
2089
  outcome: type === 'decision' ? args.outcome : undefined,
2090
+ scope: candidateScope,
1878
2091
  plan_id: candidatePlanId,
1879
2092
  model: currentModel,
1880
2093
  star_count: 0,
@@ -1885,22 +2098,25 @@ export async function executeMcpToolCall(payload) {
1885
2098
  const planPrompt = (type === 'decision' || type === 'trap') && !candidatePlanId
1886
2099
  ? `\nšŸ’” Does this ${type} relate to an active plan item? If so, re-run with planId: 'pln_xxx' to link it.`
1887
2100
  : '';
2101
+ const storeLabel = targetStore && targetStore !== 'local' ? ` [store: ${targetStore}]` : '';
1888
2102
  if (writeThrough) {
1889
- saveCandidate(candidate, cwd);
1890
- const accepted = acceptCandidate(candId.id, resolvedIdentity.agent_name, cwd, resolvedIdentity.agent_id);
1891
- 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);
1892
2106
  return {
1893
2107
  response: toolResponse({
1894
- 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}` }],
1895
2109
  candidate_id: candId.id,
1896
2110
  promoted_item_id: accepted.promoted_item_id,
1897
2111
  write_through: true,
2112
+ store: targetStore ?? 'local',
2113
+ scope: candidateScope,
1898
2114
  }),
1899
2115
  nextConnectionSessionId: explicitSessionIdFromEnv() ? undefined : identity.session_id,
1900
2116
  };
1901
2117
  }
1902
- saveCandidate(candidate, cwd);
1903
- 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);
1904
2120
  return {
1905
2121
  response: toolResponse({
1906
2122
  content: [{ type: 'text', text: `āœ” Candidate created [${candId.short_label}] (pending review)${planPrompt}` }],
@@ -2468,6 +2684,100 @@ export async function executeMcpToolCall(payload) {
2468
2684
  }),
2469
2685
  };
2470
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
+ }
2471
2781
  return {
2472
2782
  response: createToolErrorResponse('unknown_tool', `Unknown tool: ${name}`),
2473
2783
  };