brainclaw 0.25.3 → 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.
@@ -0,0 +1,75 @@
1
+ import { loadState, persistState } from '../core/state.js';
2
+ import { memoryExists } from '../core/io.js';
3
+ import { resolveTargetStore } from '../core/store-resolution.js';
4
+ import { appendAuditEntry } from '../core/audit.js';
5
+ import { resolveCurrentAgentName } from '../core/agent-registry.js';
6
+ export function runMigrate(options = {}) {
7
+ const cwd = options.cwd ?? process.cwd();
8
+ if (!memoryExists(cwd)) {
9
+ console.error('Error: .brainclaw/ not found. Run `brainclaw init` first.');
10
+ process.exit(1);
11
+ }
12
+ if (options.promoteMachineItems) {
13
+ promoteMachineItems(cwd, options.dryRun ?? false);
14
+ }
15
+ else {
16
+ console.log('Usage: brainclaw migrate --promote-machine-items [--dry-run]');
17
+ console.log('');
18
+ console.log('Moves items tagged scope:machine from project store to user store (~/.brainclaw/).');
19
+ }
20
+ }
21
+ function promoteMachineItems(cwd, dryRun) {
22
+ const state = loadState(cwd);
23
+ const agent = resolveCurrentAgentName(cwd);
24
+ const machineConstraints = state.active_constraints.filter((c) => c.scope === 'machine');
25
+ const machineDecisions = state.recent_decisions.filter((d) => d.scope === 'machine');
26
+ const machineTraps = state.known_traps.filter((t) => t.scope === 'machine');
27
+ const total = machineConstraints.length + machineDecisions.length + machineTraps.length;
28
+ if (total === 0) {
29
+ console.log('No machine-scoped items found in project store.');
30
+ return;
31
+ }
32
+ console.log(`Found ${total} machine-scoped item(s) in project store:\n`);
33
+ for (const c of machineConstraints)
34
+ console.log(` [constraint] ${c.id} — ${c.text.slice(0, 80)}`);
35
+ for (const d of machineDecisions)
36
+ console.log(` [decision] ${d.id} — ${d.text.slice(0, 80)}`);
37
+ for (const t of machineTraps)
38
+ console.log(` [trap] ${t.id} — ${t.text.slice(0, 80)}`);
39
+ if (dryRun) {
40
+ console.log(`\n(dry-run) Would move ${total} item(s) to user store.`);
41
+ return;
42
+ }
43
+ // Resolve user store
44
+ let userCwd;
45
+ try {
46
+ userCwd = resolveTargetStore(cwd, 'user');
47
+ }
48
+ catch {
49
+ console.error('Error: cannot resolve user store. Run `brainclaw setup` first.');
50
+ process.exit(1);
51
+ }
52
+ const userState = loadState(userCwd);
53
+ // Move constraints
54
+ for (const c of machineConstraints) {
55
+ userState.active_constraints.push(c);
56
+ state.active_constraints = state.active_constraints.filter((x) => x.id !== c.id);
57
+ appendAuditEntry({ actor: agent, action: 'update', item_id: c.id, item_type: 'constraint', reason: 'promote to user store (machine scope)' }, cwd);
58
+ }
59
+ // Move decisions
60
+ for (const d of machineDecisions) {
61
+ userState.recent_decisions.push(d);
62
+ state.recent_decisions = state.recent_decisions.filter((x) => x.id !== d.id);
63
+ appendAuditEntry({ actor: agent, action: 'update', item_id: d.id, item_type: 'decision', reason: 'promote to user store (machine scope)' }, cwd);
64
+ }
65
+ // Move traps
66
+ for (const t of machineTraps) {
67
+ userState.known_traps.push(t);
68
+ state.known_traps = state.known_traps.filter((x) => x.id !== t.id);
69
+ appendAuditEntry({ actor: agent, action: 'update', item_id: t.id, item_type: 'trap', reason: 'promote to user store (machine scope)' }, cwd);
70
+ }
71
+ persistState(userState, userCwd);
72
+ persistState(state, cwd);
73
+ console.log(`\n✔ Promoted ${total} item(s) to user store (${userCwd})`);
74
+ }
75
+ //# sourceMappingURL=migrate.js.map
@@ -1,11 +1,10 @@
1
- import { loadState, persistState } from '../core/state.js';
2
1
  import { resolveCurrentAgentName } from '../core/agent-registry.js';
3
2
  import { memoryExists } from '../core/io.js';
4
- import { generateIdWithLabel, nowISO } from '../core/ids.js';
5
3
  import { loadConfig } from '../core/config.js';
6
4
  import { scanText } from '../core/security.js';
7
5
  import { validateCliInput } from '../core/input-validation.js';
8
6
  import { resolveTargetStore } from '../core/store-resolution.js';
7
+ import { listTools, createTool } from '../core/registries.js';
9
8
  export function runTool(subcommand, args, options = {}) {
10
9
  const cwd = resolveTargetStore(options.cwd ?? process.cwd(), options.store ?? 'local');
11
10
  if (!memoryExists(cwd)) {
@@ -46,10 +45,7 @@ export function runTool(subcommand, args, options = {}) {
46
45
  }
47
46
  }
48
47
  function runToolList(cwd) {
49
- const state = loadState(cwd);
50
- const tools = state.recent_decisions
51
- .filter((d) => d.tags.includes('tool'))
52
- .map((d) => ({ id: d.id, name: d.text.split('\n')[0], type: d.tags.find((t) => t !== 'tool') }));
48
+ const tools = listTools(cwd);
53
49
  if (tools.length === 0) {
54
50
  console.log('No tools registered yet.');
55
51
  return;
@@ -57,9 +53,7 @@ function runToolList(cwd) {
57
53
  console.log(`\n${tools.length} tool(s):\n`);
58
54
  tools.forEach((tool) => {
59
55
  console.log(` [${tool.id}] ${tool.name}`);
60
- if (tool.type) {
61
- console.log(` type: ${tool.type}`);
62
- }
56
+ console.log(` type: ${tool.type}`);
63
57
  });
64
58
  console.log('');
65
59
  }
@@ -74,50 +68,46 @@ function runToolAdd(name, description, options, cwd) {
74
68
  process.exit(1);
75
69
  }
76
70
  }
77
- const state = loadState(cwd);
78
- const { id, short_label } = generateIdWithLabel('recent_decisions');
79
- const toolType = options.type ?? 'utility';
80
- const entry = {
81
- id,
82
- short_label,
83
- text: name,
84
- created_at: nowISO(),
71
+ const tool = createTool({
72
+ name,
73
+ description,
74
+ type: options.type,
75
+ tags: options.tag,
85
76
  author: options.author ?? resolveCurrentAgentName(cwd),
86
- tags: ['tool', toolType, ...(options.tag ?? [])],
87
- };
88
- // For now, store as decision to avoid schema migration
89
- // Will migrate to separate tool storage in v0.16
90
- state.recent_decisions.push(entry);
91
- persistState(state, cwd);
92
- console.log(`✔ Tool added: [${id}] ${name}`);
93
- console.log(' (Stored in decisions for now; will move to dedicated registry in v0.16)');
77
+ }, cwd);
78
+ console.log(`✔ Tool added: [${tool.id}] ${name} (${tool.type})`);
94
79
  }
95
80
  function runToolDescribe(toolId, cwd) {
96
- const state = loadState(cwd);
97
- const decision = state.recent_decisions.find((d) => d.id === toolId || d.short_label === toolId);
98
- if (!decision) {
81
+ const tools = listTools(cwd);
82
+ const tool = tools.find((t) => t.id === toolId || t.id.startsWith(toolId));
83
+ if (!tool) {
99
84
  console.error(`Error: tool '${toolId}' not found`);
100
85
  process.exit(1);
101
86
  }
102
- console.log(`\nTool: ${decision.text}`);
103
- console.log(`ID: ${decision.id}`);
104
- console.log(`Type: ${decision.tags.find((t) => t !== 'tool')}`);
105
- console.log(`Author: ${decision.author}`);
106
- console.log(`Created: ${decision.created_at}`);
87
+ console.log(`\nTool: ${tool.name}`);
88
+ console.log(`Description: ${tool.description}`);
89
+ console.log(`ID: ${tool.id}`);
90
+ console.log(`Type: ${tool.type}`);
91
+ console.log(`Author: ${tool.author}`);
92
+ console.log(`Created: ${tool.created_at}`);
93
+ if (tool.tags.length > 0) {
94
+ console.log(`Tags: ${tool.tags.join(', ')}`);
95
+ }
107
96
  console.log('');
108
97
  }
109
98
  function runToolSearch(query, cwd) {
110
- const state = loadState(cwd);
111
- const tools = state.recent_decisions.filter((d) => d.tags.includes('tool'));
112
- const results = tools.filter((tool) => tool.text.toLowerCase().includes(query.toLowerCase()) ||
113
- tool.tags.some((tag) => tag.toLowerCase().includes(query.toLowerCase())));
99
+ const tools = listTools(cwd);
100
+ const queryLower = query.toLowerCase();
101
+ const results = tools.filter((tool) => tool.name.toLowerCase().includes(queryLower) ||
102
+ tool.description.toLowerCase().includes(queryLower) ||
103
+ tool.tags.some((tag) => tag.toLowerCase().includes(queryLower)));
114
104
  if (results.length === 0) {
115
105
  console.log(`No tools found matching '${query}'`);
116
106
  return;
117
107
  }
118
108
  console.log(`\n${results.length} tool(s) matching '${query}':\n`);
119
- results.forEach((result) => {
120
- console.log(` [${result.id}] ${result.text.split('\n')[0]}`);
109
+ results.forEach((tool) => {
110
+ console.log(` [${tool.id}] ${tool.name} (${tool.type})`);
121
111
  });
122
112
  console.log('');
123
113
  }
@@ -441,6 +441,12 @@ function buildCommandHookEntry(command) {
441
441
  hooks: [{ type: 'command', command }],
442
442
  };
443
443
  }
444
+ function buildMatchedCommandHookEntry(matcher, command) {
445
+ return {
446
+ matcher,
447
+ hooks: [{ type: 'command', command }],
448
+ };
449
+ }
444
450
  function containsCommandHook(entries, command) {
445
451
  return entries.some((entry) => isJsonObject(entry) &&
446
452
  Array.isArray(entry.hooks) &&
@@ -582,6 +588,13 @@ export function ensureClaudeCodeSettings(cwd) {
582
588
  stopHooks.push(buildCommandHookEntry(stopCommand));
583
589
  }
584
590
  hooks.Stop = stopHooks;
591
+ // PostToolUse — check for unseen events after any brainclaw MCP tool call
592
+ const checkEventsCommand = 'npx brainclaw check-events 2>/dev/null';
593
+ const postToolHooks = Array.isArray(hooks.PostToolUse) ? [...hooks.PostToolUse] : [];
594
+ if (!containsCommandHook(postToolHooks, checkEventsCommand)) {
595
+ postToolHooks.push(buildMatchedCommandHookEntry('mcp__brainclaw__', checkEventsCommand));
596
+ }
597
+ hooks.PostToolUse = postToolHooks;
585
598
  const { created, updated } = writeJsonFileIfChanged(filePath, {
586
599
  ...existing,
587
600
  permissions,
@@ -13,7 +13,7 @@ import { inferProjectFromTarget, loadInstructions, resolveInstructions } from '.
13
13
  import { buildCurrentAgentResumeSummary, buildReputationRankingLookup } from './reputation.js';
14
14
  import { loadState } from './state.js';
15
15
  import { listCandidates } from './candidates.js';
16
- import { listClaims } from './claims.js';
16
+ import { listClaims, isClaimExpired } from './claims.js';
17
17
  import { listRuntimeNotes } from './runtime.js';
18
18
  import { isTrapActive, listOperationalTraps } from './traps.js';
19
19
  import { buildEstimationReport } from '../commands/estimation-report.js';
@@ -67,6 +67,7 @@ export function buildContext(options = {}) {
67
67
  score: 0,
68
68
  reasons: [],
69
69
  extra: meta.join(', '),
70
+ provenance: { actor: plan.author },
70
71
  });
71
72
  }
72
73
  for (const c of state.active_constraints) {
@@ -316,10 +317,36 @@ export function buildContext(options = {}) {
316
317
  items.splice(0, items.length, ...items.filter((i) => allowed.includes(i.section)));
317
318
  }
318
319
  const queryTerms = tokenise(target);
320
+ // Agent-layer scoring: boost items related to the current agent's claims
321
+ const agentName = agent;
322
+ const agentId = currentAgentIdentity?.agent_id;
323
+ const allClaims = [...listClaims(contextCwd), ...parentStoreClaims];
324
+ const myClaims = allClaims.filter((c) => c.status === 'active' && (agentId ? c.agent_id === agentId : c.agent === agentName));
325
+ const myClaimScopes = myClaims.map((c) => c.scope);
326
+ const otherActiveClaims = allClaims.filter((c) => c.status === 'active' && !(agentId ? c.agent_id === agentId : c.agent === agentName));
319
327
  for (const item of items) {
320
328
  const relevance = computeRelevance(item, queryTerms, profile, target);
321
329
  item.score = relevance.score;
322
330
  item.reasons = relevance.reasons;
331
+ // Layer 1: boost items in my claimed scope (+6)
332
+ if (item.score >= 0 && myClaimScopes.length > 0 && item.related_paths) {
333
+ const overlaps = item.related_paths.some((p) => myClaimScopes.some((scope) => p.includes(scope) || scope.includes(p)));
334
+ if (overlaps) {
335
+ item.score += 6;
336
+ item.reasons = uniqueReasons([...item.reasons, 'agent-layer: my claimed scope']);
337
+ }
338
+ }
339
+ // Layer 1: boost plans assigned to me (+5)
340
+ if (item.score >= 0 && item.section === 'plan' && item.extra?.includes(`assignee:${agentName}`)) {
341
+ item.score += 5;
342
+ item.reasons = uniqueReasons([...item.reasons, 'agent-layer: my assigned plan']);
343
+ }
344
+ // Layer 2: boost items authored by me (+2)
345
+ if (item.score >= 0 && item.provenance?.actor === agentName) {
346
+ item.score += 2;
347
+ item.reasons = uniqueReasons([...item.reasons, 'agent-layer: my authored item']);
348
+ }
349
+ // Reputation signal
323
350
  if (item.score >= 0 && item.provenance) {
324
351
  const trustBonus = rankingLookup.getRankingBonus(item.provenance.actor_id, item.provenance.actor);
325
352
  if (trustBonus > 0) {
@@ -327,6 +354,14 @@ export function buildContext(options = {}) {
327
354
  item.reasons = uniqueReasons([...item.reasons, `reputation signal:+${trustBonus.toFixed(2)}`]);
328
355
  }
329
356
  }
357
+ // Layer 3: boost machine-scoped items for broader visibility (+1)
358
+ if (item.score >= 0) {
359
+ const itemScope = item.scope;
360
+ if (itemScope === 'machine') {
361
+ item.score += 1;
362
+ item.reasons = uniqueReasons([...item.reasons, 'machine-scope signal']);
363
+ }
364
+ }
330
365
  }
331
366
  const ranked = items
332
367
  .filter(item => item.score >= 0)
@@ -384,18 +419,15 @@ export function buildContext(options = {}) {
384
419
  ? summariseAgentTooling(rawAgentTooling)
385
420
  : undefined;
386
421
  // Build open_work: active claims and in_progress plans owned by the current agent
422
+ // Reuses myClaims computed in agent-layer scoring above
387
423
  let openWork;
388
424
  if (currentAgentIdentity || agent) {
389
- const agentName = agent;
390
- const agentId = currentAgentIdentity?.agent_id;
391
- const allClaims = [...listClaims(contextCwd), ...parentStoreClaims];
392
- const activeClaims = allClaims.filter((c) => c.status === 'active' && (agentId ? c.agent_id === agentId : c.agent === agentName));
393
- const claimPlanIds = new Set(activeClaims.map((c) => c.plan_id).filter(Boolean));
425
+ const claimPlanIds = new Set(myClaims.map((c) => c.plan_id).filter(Boolean));
394
426
  const inProgressPlans = state.plan_items.filter((p) => p.status === 'in_progress' &&
395
427
  (p.assignee === agentName || claimPlanIds.has(p.id)));
396
- if (activeClaims.length > 0 || inProgressPlans.length > 0) {
428
+ if (myClaims.length > 0 || inProgressPlans.length > 0) {
397
429
  openWork = {
398
- active_claims: activeClaims.map(({ id, scope, description, created_at, plan_id, expires_at }) => ({ id, scope, description, created_at, plan_id, expires_at })),
430
+ active_claims: myClaims.map(({ id, scope, description, created_at, plan_id, expires_at }) => ({ id, scope, description, created_at, plan_id, expires_at })),
399
431
  in_progress_plans: inProgressPlans.map(({ id, text, assignee }) => ({ id, text, assignee })),
400
432
  };
401
433
  }
@@ -471,6 +503,8 @@ export function buildContext(options = {}) {
471
503
  }
472
504
  })(),
473
505
  cross_project_items: crossProjectItems.length > 0 ? crossProjectItems : undefined,
506
+ claim_conflicts: detectClaimConflicts(myClaims, otherActiveClaims),
507
+ workflow_hints: buildWorkflowHints(myClaims, openWork, state.plan_items),
474
508
  selected,
475
509
  };
476
510
  if (options.digest) {
@@ -1232,4 +1266,64 @@ function applyCharBudget(items, maxChars) {
1232
1266
  }
1233
1267
  return selected;
1234
1268
  }
1269
+ // --- Claim conflict detection ---
1270
+ function detectClaimConflicts(myClaims, otherClaims) {
1271
+ if (myClaims.length === 0 || otherClaims.length === 0)
1272
+ return undefined;
1273
+ const conflicts = [];
1274
+ for (const mine of myClaims) {
1275
+ for (const other of otherClaims) {
1276
+ if (isClaimExpired(other))
1277
+ continue;
1278
+ const overlap = scopesOverlap(mine.scope, other.scope);
1279
+ if (overlap) {
1280
+ conflicts.push({
1281
+ my_claim_id: mine.id,
1282
+ my_scope: mine.scope,
1283
+ other_claim_id: other.id,
1284
+ other_agent: other.agent,
1285
+ other_scope: other.scope,
1286
+ overlap_reason: overlap,
1287
+ });
1288
+ }
1289
+ }
1290
+ }
1291
+ return conflicts.length > 0 ? conflicts : undefined;
1292
+ }
1293
+ function scopesOverlap(a, b) {
1294
+ const aParts = a.replace(/\\/g, '/').split(/\s+/);
1295
+ const bParts = b.replace(/\\/g, '/').split(/\s+/);
1296
+ for (const ap of aParts) {
1297
+ for (const bp of bParts) {
1298
+ if (ap === bp)
1299
+ return `exact match: ${ap}`;
1300
+ if (ap.startsWith(bp + '/') || bp.startsWith(ap + '/'))
1301
+ return `path overlap: ${ap} ↔ ${bp}`;
1302
+ }
1303
+ }
1304
+ return null;
1305
+ }
1306
+ // --- Workflow hints ---
1307
+ function buildWorkflowHints(myClaims, openWork, plans) {
1308
+ const hints = [];
1309
+ // No claims — suggest claiming before editing
1310
+ if (myClaims.length === 0) {
1311
+ const todoPlans = plans.filter((p) => p.status === 'todo' && p.priority === 'high');
1312
+ if (todoPlans.length > 0) {
1313
+ hints.push(`${todoPlans.length} high-priority plan(s) available — consider claiming one with bclaw_claim`);
1314
+ }
1315
+ }
1316
+ // Multiple unclosed claims — suggest releasing finished ones
1317
+ if (myClaims.length > 2) {
1318
+ hints.push(`You have ${myClaims.length} active claims — consider releasing finished ones with bclaw_release_claim`);
1319
+ }
1320
+ // In-progress plans without claims
1321
+ if (openWork) {
1322
+ const unclaimedInProgress = openWork.in_progress_plans.filter((p) => !openWork.active_claims.some((c) => c.plan_id === p.id));
1323
+ if (unclaimedInProgress.length > 0) {
1324
+ hints.push(`${unclaimedInProgress.length} in-progress plan(s) without a claim — consider claiming the scope you're editing`);
1325
+ }
1326
+ }
1327
+ return hints.length > 0 ? hints : undefined;
1328
+ }
1235
1329
  //# sourceMappingURL=context.js.map
@@ -81,6 +81,31 @@ export function buildCoordinationSnapshot(options = {}) {
81
81
  resolved_instructions: instructions,
82
82
  reputation_summary: reputationSummary,
83
83
  agent_reputation: agentReputation,
84
+ other_agents: buildOtherAgentsSummary(filteredClaims, filteredNotes, agent),
84
85
  };
85
86
  }
87
+ function buildOtherAgentsSummary(claims, notes, currentAgent) {
88
+ const agentMap = new Map();
89
+ for (const claim of claims) {
90
+ if (claim.agent === currentAgent)
91
+ continue;
92
+ const existing = agentMap.get(claim.agent) ?? { name: claim.agent, claim_count: 0, scopes: [] };
93
+ existing.claim_count++;
94
+ existing.scopes.push(claim.scope);
95
+ if (!existing.last_active || claim.created_at > existing.last_active) {
96
+ existing.last_active = claim.created_at;
97
+ }
98
+ agentMap.set(claim.agent, existing);
99
+ }
100
+ for (const note of notes) {
101
+ if (note.agent === currentAgent)
102
+ continue;
103
+ const existing = agentMap.get(note.agent);
104
+ if (existing && (!existing.last_active || note.created_at > existing.last_active)) {
105
+ existing.last_active = note.created_at;
106
+ }
107
+ }
108
+ const result = [...agentMap.values()];
109
+ return result.length > 0 ? result : undefined;
110
+ }
86
111
  //# sourceMappingURL=coordination.js.map
package/dist/core/io.js CHANGED
@@ -35,6 +35,8 @@ const ENTITY_DIR_MAP = {
35
35
  // discovery/ — Project entity: what's available
36
36
  'bootstrap': 'discovery/bootstrap',
37
37
  'bootstrap/seeds': 'discovery/bootstrap/seeds',
38
+ 'capabilities': 'discovery/capabilities',
39
+ 'tools': 'discovery/tools',
38
40
  // agents/ — stays at top level (already entity-aligned)
39
41
  'agents': 'agents',
40
42
  };
@@ -2,7 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import YAML from 'yaml';
4
4
  import { memoryDir, memoryPath, readFileSync, writeFileAtomic, resolveEntityDir } from './io.js';
5
- import { BootstrapApplicationReceiptSchema, BootstrapImportPlanDocumentSchema, AgentIdentityDocumentSchema, BootstrapProfileDocumentSchema, CandidateSchema, ClaimSchema, ConfigSchema, MemorySeedDocumentSchema, ConstraintSchema, CurrentSessionStateSchema, DecisionSchema, HandoffSchema, InstructionEntrySchema, PlanItemSchema, ProjectIdentityDocumentSchema, RuntimeNoteSchema, SessionSnapshotSchema, TrapSchema, AiSurfaceTaskRequestSchema, } from './schema.js';
5
+ import { BootstrapApplicationReceiptSchema, BootstrapImportPlanDocumentSchema, AgentIdentityDocumentSchema, BootstrapProfileDocumentSchema, CandidateSchema, ClaimSchema, ConfigSchema, MemorySeedDocumentSchema, ConstraintSchema, CurrentSessionStateSchema, DecisionSchema, HandoffSchema, InstructionEntrySchema, PlanItemSchema, ProjectIdentityDocumentSchema, RuntimeNoteSchema, SessionSnapshotSchema, TrapSchema, AiSurfaceTaskRequestSchema, ProjectCapabilitySchema, ProjectToolSchema, } from './schema.js';
6
6
  export class MigrationError extends Error {
7
7
  kind;
8
8
  documentType;
@@ -33,6 +33,8 @@ const registry = {
33
33
  runtime_note: createRegistryEntry(RuntimeNoteSchema),
34
34
  ai_surface_task: createRegistryEntry(AiSurfaceTaskRequestSchema),
35
35
  session_snapshot: createRegistryEntry(SessionSnapshotSchema),
36
+ capability: createRegistryEntry(ProjectCapabilitySchema),
37
+ tool: createRegistryEntry(ProjectToolSchema),
36
38
  trap: createRegistryEntry(TrapSchema),
37
39
  };
38
40
  function createRegistryEntry(schema) {
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Project discovery — unified workspace inventory that composes existing
3
+ * scan functions into a single structured profile.
4
+ *
5
+ * Boundary: discovery describes what exists in the workspace RIGHT NOW.
6
+ * It is NOT canonical memory (decisions, traps, plans). It is NOT
7
+ * machine profile (shells, SSH keys, WSL distros). It is the project-level
8
+ * answer to "what MCP servers, skills, hooks, instruction files, and
9
+ * agent integrations are available in this workspace?"
10
+ */
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import { buildAgentToolingContext } from './agent-context.js';
14
+ import { assessAgentIntegrationReadiness } from './agent-integrations.js';
15
+ import { loadConfig } from './config.js';
16
+ import { memoryDir } from './io.js';
17
+ import { nowISO } from './ids.js';
18
+ // --- Native instruction file discovery (extracted from bootstrap.ts) ---
19
+ const NATIVE_INSTRUCTION_FILES = [
20
+ 'AGENTS.md',
21
+ 'CLAUDE.md',
22
+ 'GEMINI.md',
23
+ '.windsurfrules',
24
+ '.github/copilot-instructions.md',
25
+ ];
26
+ const NATIVE_INSTRUCTION_DIRS = [
27
+ '.cursor/rules',
28
+ '.roo/rules',
29
+ '.continue/rules',
30
+ '.clinerules',
31
+ ];
32
+ // MCP config files that agents use
33
+ const MCP_CONFIG_FILES = [
34
+ '.mcp.json',
35
+ 'opencode.json',
36
+ '.cursor/mcp.json',
37
+ '.roo/mcp.json',
38
+ '.continue/config.json',
39
+ ];
40
+ // Hook config files
41
+ const HOOK_CONFIG_FILES = [
42
+ '.claude/settings.local.json',
43
+ '.cursor/rules/brainclaw-session.mdc',
44
+ ];
45
+ export function buildProjectDiscovery(options = {}) {
46
+ const cwd = options.cwd ?? process.cwd();
47
+ const env = options.env ?? process.env;
48
+ // 1. Agent tooling (AGENTS.md, skills, MCP servers)
49
+ const agentTooling = buildAgentToolingContext({ cwd, env });
50
+ // 2. Native instruction files
51
+ const instructionFiles = discoverFiles(cwd, NATIVE_INSTRUCTION_FILES, NATIVE_INSTRUCTION_DIRS);
52
+ // 3. MCP config files
53
+ const mcpConfigs = discoverStaticFiles(cwd, MCP_CONFIG_FILES);
54
+ // 4. Hook config files
55
+ const hookConfigs = discoverStaticFiles(cwd, HOOK_CONFIG_FILES);
56
+ // 5. Integration readiness
57
+ let integrations = [];
58
+ try {
59
+ const config = loadConfig(cwd);
60
+ integrations = assessAgentIntegrationReadiness(config, cwd, env);
61
+ }
62
+ catch {
63
+ // config may not exist yet
64
+ }
65
+ const foundInstructions = instructionFiles.filter(f => f.exists);
66
+ const foundMcpConfigs = mcpConfigs.filter(f => f.exists);
67
+ const foundHookConfigs = hookConfigs.filter(f => f.exists);
68
+ return {
69
+ discovered_at: nowISO(),
70
+ workspace_root: cwd,
71
+ agent_tooling: agentTooling,
72
+ instruction_files: foundInstructions,
73
+ mcp_configs: foundMcpConfigs,
74
+ hook_configs: foundHookConfigs,
75
+ integrations,
76
+ summary: {
77
+ total_instruction_files: foundInstructions.length,
78
+ total_mcp_servers: agentTooling.mcp_servers.length,
79
+ total_skills: agentTooling.skills.length,
80
+ total_mcp_configs: foundMcpConfigs.length,
81
+ total_hook_configs: foundHookConfigs.length,
82
+ integrations_ready: integrations.filter(i => i.ready).length,
83
+ integrations_total: integrations.length,
84
+ },
85
+ };
86
+ }
87
+ // --- Persistence ---
88
+ const DISCOVERY_PROFILE_FILE = 'discovery-profile.json';
89
+ export function saveDiscoveryProfile(profile, cwd) {
90
+ const dir = path.join(memoryDir(cwd), 'discovery');
91
+ if (!fs.existsSync(dir)) {
92
+ fs.mkdirSync(dir, { recursive: true });
93
+ }
94
+ const filePath = path.join(dir, DISCOVERY_PROFILE_FILE);
95
+ fs.writeFileSync(filePath, JSON.stringify(profile, null, 2), 'utf-8');
96
+ }
97
+ export function loadDiscoveryProfile(cwd) {
98
+ const filePath = path.join(memoryDir(cwd), 'discovery', DISCOVERY_PROFILE_FILE);
99
+ if (!fs.existsSync(filePath))
100
+ return undefined;
101
+ try {
102
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
103
+ }
104
+ catch {
105
+ return undefined;
106
+ }
107
+ }
108
+ // --- Rendering ---
109
+ export function renderDiscoverySummary(profile) {
110
+ const lines = [];
111
+ const s = profile.summary;
112
+ lines.push('# Project Discovery');
113
+ lines.push(`Discovered at: ${profile.discovered_at}`);
114
+ lines.push('');
115
+ // Instruction files
116
+ if (profile.instruction_files.length > 0) {
117
+ lines.push(`## Instruction Files (${s.total_instruction_files})`);
118
+ for (const f of profile.instruction_files) {
119
+ const managed = f.managed_by_brainclaw ? ' (managed)' : '';
120
+ lines.push(` ${f.path}${managed}`);
121
+ }
122
+ lines.push('');
123
+ }
124
+ // MCP configs
125
+ if (profile.mcp_configs.length > 0) {
126
+ lines.push(`## MCP Configs (${s.total_mcp_configs})`);
127
+ for (const f of profile.mcp_configs) {
128
+ lines.push(` ${f.path}`);
129
+ }
130
+ lines.push('');
131
+ }
132
+ // MCP servers (from agent tooling)
133
+ if (s.total_mcp_servers > 0) {
134
+ lines.push(`## MCP Servers (${s.total_mcp_servers})`);
135
+ for (const server of profile.agent_tooling.mcp_servers) {
136
+ lines.push(` ${server.name} (${server.transport}, ${server.availability})`);
137
+ }
138
+ lines.push('');
139
+ }
140
+ // Skills
141
+ if (s.total_skills > 0) {
142
+ lines.push(`## Skills (${s.total_skills})`);
143
+ for (const skill of profile.agent_tooling.skills.slice(0, 10)) {
144
+ lines.push(` ${skill.name}${skill.description ? `: ${skill.description}` : ''}`);
145
+ }
146
+ if (s.total_skills > 10) {
147
+ lines.push(` ... and ${s.total_skills - 10} more`);
148
+ }
149
+ lines.push('');
150
+ }
151
+ // Hook configs
152
+ if (profile.hook_configs.length > 0) {
153
+ lines.push(`## Hook Configs (${s.total_hook_configs})`);
154
+ for (const f of profile.hook_configs) {
155
+ lines.push(` ${f.path}`);
156
+ }
157
+ lines.push('');
158
+ }
159
+ // Integrations
160
+ if (profile.integrations.length > 0) {
161
+ lines.push(`## Agent Integrations (${s.integrations_ready}/${s.integrations_total} ready)`);
162
+ for (const integ of profile.integrations) {
163
+ const status = integ.ready ? '✔' : '✗';
164
+ const missing = integ.missing_surfaces.length > 0
165
+ ? ` — missing: ${integ.missing_surfaces.map(s => s.kind).join(', ')}`
166
+ : '';
167
+ lines.push(` ${status} ${integ.agent_name}${missing}`);
168
+ }
169
+ lines.push('');
170
+ }
171
+ if (s.total_instruction_files + s.total_mcp_servers + s.total_skills === 0) {
172
+ lines.push('No integration surfaces detected.');
173
+ }
174
+ return lines.join('\n');
175
+ }
176
+ // --- Helpers ---
177
+ function discoverStaticFiles(cwd, files) {
178
+ const results = [];
179
+ for (const relativePath of files) {
180
+ const fullPath = path.join(cwd, relativePath);
181
+ if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
182
+ const stat = fs.statSync(fullPath);
183
+ results.push({
184
+ path: relativePath,
185
+ exists: true,
186
+ size: stat.size,
187
+ managed_by_brainclaw: isManagedByBrainclaw(fullPath),
188
+ });
189
+ }
190
+ }
191
+ return results;
192
+ }
193
+ function discoverFiles(cwd, files, dirs) {
194
+ const results = [];
195
+ for (const relativePath of files) {
196
+ const fullPath = path.join(cwd, relativePath);
197
+ if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
198
+ results.push({
199
+ path: relativePath,
200
+ exists: true,
201
+ size: fs.statSync(fullPath).size,
202
+ managed_by_brainclaw: isManagedByBrainclaw(fullPath),
203
+ });
204
+ }
205
+ }
206
+ for (const relativeDir of dirs) {
207
+ const dir = path.join(cwd, relativeDir);
208
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory())
209
+ continue;
210
+ for (const entry of fs.readdirSync(dir).sort()) {
211
+ if (!/\.(md|mdc)$/i.test(entry))
212
+ continue;
213
+ const relativePath = path.posix.join(relativeDir.replace(/\\/g, '/'), entry);
214
+ const fullPath = path.join(cwd, relativePath);
215
+ results.push({
216
+ path: relativePath,
217
+ exists: true,
218
+ size: fs.statSync(fullPath).size,
219
+ managed_by_brainclaw: isManagedByBrainclaw(fullPath),
220
+ });
221
+ }
222
+ }
223
+ return results;
224
+ }
225
+ function isManagedByBrainclaw(filePath) {
226
+ try {
227
+ const content = fs.readFileSync(filePath, 'utf-8').slice(0, 200);
228
+ return content.includes('brainclaw') && (content.includes('Managed by brainclaw') ||
229
+ content.includes('BRAINCLAW_SECTION') ||
230
+ content.includes('brainclaw export'));
231
+ }
232
+ catch {
233
+ return false;
234
+ }
235
+ }
236
+ //# sourceMappingURL=project-discovery.js.map