brainclaw 1.11.1 → 1.12.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.
Binary file
@@ -7,14 +7,14 @@ import { fileURLToPath } from 'node:url';
7
7
  import { Worker } from 'node:worker_threads';
8
8
  import { generatedSchemas } from './mcp-schemas.generated.js';
9
9
  import { getTriggeredItems, renderTriggeredItems } from '../core/lifecycle.js';
10
- import { resolveCrossProjectWritableTarget, resolveProjectCwd, writeCrossProjectSignal } from '../core/cross-project.js';
10
+ import { resolveCrossProjectLinks, resolveCrossProjectWritableTarget, resolveProjectCwd, writeCrossProjectSignal } from '../core/cross-project.js';
11
11
  import { buildContext, renderContextMarkdown, renderContextPromptTemplate, renderContextBriefing } from '../core/context.js';
12
12
  import { buildCoordinationSnapshot } from '../core/coordination.js';
13
13
  import { checkBrainclawInstallableUpdate, getInstalledBrainclawVersion, readDiskBrainclawVersion, renderBrainclawInstallableUpdateNotice } from '../core/brainclaw-version.js';
14
14
  import { loadConfig } from '../core/config.js';
15
15
  import { collectLoadValidationWarnings, findLoadValidationWarning, loadState, persistState, saveState } from '../core/state.js';
16
16
  import { generateIdWithLabel } from '../core/ids.js';
17
- import { memoryExists } from '../core/io.js';
17
+ import { memoryExists, MEMORY_DIR } from '../core/io.js';
18
18
  import { generateCandidateIdWithLabel, loadCandidate, saveCandidate } from '../core/candidates.js';
19
19
  import { createEntity, getEntity, listEntities, boundListResult, DEFAULT_FIND_CHAR_BUDGET, removeEntity, transitionEntity, updateEntity, } from '../core/entity-operations.js';
20
20
  import { relocateEntity } from '../core/operations/relocate.js';
@@ -40,7 +40,8 @@ import { createCapability, createTool as createRegistryTool } from '../core/regi
40
40
  import { detectAiAgent } from '../core/ai-agent-detection.js';
41
41
  import { checkGitPresence, scanGitRepos, parseRoots, parseRepoSelection, parseAgentSelection, getDetectedSetupAgentNames, getInstalledAgentNames, runGlobalInstall, initReposAndConfigureAgents, readSetupState, ALL_KNOWN_AGENTS, } from './setup.js';
42
42
  import { buildAgentInventory } from '../core/agent-inventory.js';
43
- import { resolveEffectiveCwd, resolveEffectiveCwdInfo, resolveProjectRef, resolveTargetStore } from '../core/store-resolution.js';
43
+ import { findOutermostBrainclawRoot, resolveEffectiveCwd, resolveEffectiveCwdInfo, resolveProjectRef, resolveTargetStore } from '../core/store-resolution.js';
44
+ import { switchProject } from './switch.js';
44
45
  import { assessBootstrapNeed, probeForQuickSetup, buildQuickSetupProbeResponse, buildOnboardingPreview, resolveEmptyMemoryRecommendation } from '../core/setup-flow.js';
45
46
  import { ensureUserStore, resolveHomeDir } from '../core/setup-state.js';
46
47
  import { createPlan, addStep as addStepOp, completeStep as completeStepOp, updateStep as updateStepOp, deleteStep as deleteStepOp, deletePlan as deletePlanOp } from '../core/operations/plan.js';
@@ -2647,6 +2648,100 @@ function blockCrossProjectExecution(entity, args) {
2647
2648
  return createToolErrorResponse('validation_error', error instanceof Error ? error.message : String(error));
2648
2649
  }
2649
2650
  }
2651
+ function matchesCrossProjectLink(ref, cwd) {
2652
+ const trimmed = ref.trim();
2653
+ if (!trimmed)
2654
+ return false;
2655
+ const linkRoots = new Set([path.resolve(cwd), path.resolve(resolveWorkspaceAnchor(cwd))]);
2656
+ for (const root of linkRoots) {
2657
+ for (const link of resolveCrossProjectLinks(root)) {
2658
+ if (link.projectName === trimmed
2659
+ || link.name === trimmed
2660
+ || link.path === trimmed
2661
+ || link.absolutePath === trimmed
2662
+ || path.basename(link.absolutePath) === trimmed) {
2663
+ return true;
2664
+ }
2665
+ }
2666
+ }
2667
+ return false;
2668
+ }
2669
+ /**
2670
+ * Resolve the destination store for an execution-entity write (plan / claim and
2671
+ * their sub-objects: steps).
2672
+ *
2673
+ * The signaling-only boundary (cnd cross_project_signaling_vs_execution) forbids
2674
+ * driving execution entities into ANOTHER project — but that rule is about
2675
+ * FEDERATION (cross_project_links / other machines), not about workspace siblings
2676
+ * in the same monorepo. Switching into a sibling and creating a plan there is a
2677
+ * purely local operation, and the one the agent actually wants.
2678
+ *
2679
+ * So when `project=X` resolves to a workspace store-chain child (resolveProjectRef
2680
+ * hits — it only matches projects reachable WITHIN this workspace, never a
2681
+ * federated link), we AUTO-LOCALIZE: open a session + session-scoped switch into X
2682
+ * (sticky, per-agent — switchProject auto-creates the session if missing), then
2683
+ * write locally in X. Federated links and unknown names stay blocked.
2684
+ *
2685
+ * DGX dogfood 2026-06-27: without this an agent on the /srv monorepo cannot
2686
+ * `bclaw_create(entity=plan, project=<child>)` — it was rejected as cross-project —
2687
+ * so plans silently fell back to the default project instead.
2688
+ */
2689
+ function resolveExecutionWriteTarget(entity, args, cwd, connectionSessionId) {
2690
+ const targetProject = getCrossProjectArg(args, 'targetProject', 'target_project', 'crossProject', 'cross_project', 'project');
2691
+ if (!targetProject) {
2692
+ return { targetCwd: cwd, autoSwitched: false };
2693
+ }
2694
+ if (matchesCrossProjectLink(targetProject, cwd)) {
2695
+ const block = blockCrossProjectExecution(entity, args);
2696
+ return {
2697
+ block: block ?? createToolErrorResponse('validation_error', `Cross-project execution write blocked: ${targetProject}`),
2698
+ targetCwd: cwd,
2699
+ autoSwitched: false,
2700
+ };
2701
+ }
2702
+ // Workspace store-chain child (or the workspace root / the current project)?
2703
+ const wsHit = resolveProjectRef(targetProject, cwd);
2704
+ if (wsHit) {
2705
+ // Same-workspace → auto-localize. Switch the session into X (sticky,
2706
+ // session-scoped) so subsequent un-qualified writes follow, and persist the
2707
+ // session under the workspace anchor where resolveEffectiveCwd probes for it
2708
+ // — NOT the effective child cwd, or stickiness would be invisible on the next
2709
+ // call. The switch is best-effort: the write still localizes to wsHit below.
2710
+ let autoSwitched = false;
2711
+ try {
2712
+ const anchor = resolveWorkspaceAnchor(cwd);
2713
+ const sessionId = connectionSessionId ?? explicitSessionIdFromEnv();
2714
+ switchProject(targetProject, { cwd: anchor, sessionOnly: true, sessionId });
2715
+ autoSwitched = true;
2716
+ }
2717
+ catch {
2718
+ /* sticky switch is best-effort */
2719
+ }
2720
+ return { targetCwd: wsHit, autoSwitched, resolvedProject: projectInfoForCwd(wsHit) };
2721
+ }
2722
+ // Not a workspace child → federated link or unknown name. The signaling-only
2723
+ // boundary stands: execution entities never cross a federation boundary.
2724
+ const block = blockCrossProjectExecution(entity, args);
2725
+ return {
2726
+ block: block ?? createToolErrorResponse('validation_error', `Unknown project: ${targetProject}`),
2727
+ targetCwd: cwd,
2728
+ autoSwitched: false,
2729
+ };
2730
+ }
2731
+ /**
2732
+ * Workspace anchor for persisting a session switch — mirrors resolveEffectiveCwd's
2733
+ * anchor selection (BRAINCLAW_CWD when it is a real store, else the outermost
2734
+ * store walking up from cwd) so an auto-switch is found on the next resolution.
2735
+ */
2736
+ function resolveWorkspaceAnchor(cwd) {
2737
+ const env = process.env.BRAINCLAW_CWD?.trim();
2738
+ if (env) {
2739
+ const resolved = path.resolve(env);
2740
+ if (fs.existsSync(path.join(resolved, MEMORY_DIR, 'config.yaml')))
2741
+ return resolved;
2742
+ }
2743
+ return findOutermostBrainclawRoot(cwd) ?? cwd;
2744
+ }
2650
2745
  // Read handlers moved to mcp-read-handlers.ts
2651
2746
  import { handleMcpReadToolCall } from './mcp-read-handlers.js';
2652
2747
  export { handleMcpReadToolCall };
@@ -3300,21 +3395,17 @@ async function _executeMcpToolCallInner(payload) {
3300
3395
  }
3301
3396
  }
3302
3397
  if (name === 'bclaw_claim') {
3303
- const crossProjectError = blockCrossProjectExecution('claim', args);
3304
- if (crossProjectError) {
3305
- return { response: crossProjectError };
3306
- }
3307
- // Resolve project-scoped cwd before store resolution (fixes worktree in wrong project)
3308
- let effectiveClaimCwd = cwd;
3309
- const claimProjectArg = args.project;
3310
- if (claimProjectArg) {
3311
- const resolvedProject = resolveProjectRef(claimProjectArg, cwd);
3312
- if (resolvedProject)
3313
- effectiveClaimCwd = resolvedProject;
3314
- }
3398
+ // project=X naming a workspace sibling auto-localizes (session+switch then
3399
+ // claim locally); federated links / unknown names stay blocked.
3400
+ const claimLoc = resolveExecutionWriteTarget('claim', args, cwd, connectionSessionId);
3401
+ if (claimLoc.block) {
3402
+ return { response: claimLoc.block };
3403
+ }
3404
+ const effectiveClaimCwd = claimLoc.targetCwd;
3405
+ const claimAutoSwitched = claimLoc.autoSwitched;
3315
3406
  const storeTarget = args.store ?? 'local';
3316
3407
  const claimCwd = resolveTargetStore(effectiveClaimCwd, storeTarget);
3317
- const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', claimCwd, connectionSessionId);
3408
+ const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
3318
3409
  if (resolved.error) {
3319
3410
  return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
3320
3411
  }
@@ -3329,10 +3420,13 @@ async function _executeMcpToolCallInner(payload) {
3329
3420
  return { response: createToolErrorResponse('validation_error', descCheck.message) };
3330
3421
  }
3331
3422
  const resolvedIdentity = resolved.identity;
3332
- const identity = buildOperationalIdentity(resolvedIdentity.agent_name, claimCwd, {
3333
- agentId: resolvedIdentity.agent_id,
3334
- sessionId: connectionSessionId,
3335
- });
3423
+ const identity = {
3424
+ ...buildOperationalIdentity(resolvedIdentity.agent_name, cwd, {
3425
+ agentId: resolvedIdentity.agent_id,
3426
+ sessionId: connectionSessionId,
3427
+ }),
3428
+ project_id: loadConfig(claimCwd).project_id,
3429
+ };
3336
3430
  const claimId = generateClaimId();
3337
3431
  let worktreePath;
3338
3432
  let worktreeWarn = '';
@@ -3438,7 +3532,8 @@ async function _executeMcpToolCallInner(payload) {
3438
3532
  const worktreeNote = worktreePath ? `\n Worktree: ${worktreePath}` : '';
3439
3533
  const expiryNote = claimExpiresAt ? `\n Expires: ${claimExpiresAt.slice(0, 16).replace('T', ' ')} UTC` : '';
3440
3534
  const handoffNote = handoffMode ? `\n Handoff: ${handoffMode} (another agent will review and merge)` : '';
3441
- const claimText = `✔ Claimed scope [${claimId}]${worktreeNote}${expiryNote}${handoffNote}${noPlanWarn}${worktreeWarn}${branchWarn}${staleBranchWarn}${policyWarn}${postClaimText ? `\n${postClaimText}` : ''}`;
3535
+ const autoSwitchNote = claimAutoSwitched ? `\n Auto-switched ${projectInfoForCwd(effectiveClaimCwd).name ?? effectiveClaimCwd}` : '';
3536
+ const claimText = `✔ Claimed scope [${claimId}]${worktreeNote}${expiryNote}${handoffNote}${autoSwitchNote}${noPlanWarn}${worktreeWarn}${branchWarn}${staleBranchWarn}${policyWarn}${postClaimText ? `\n${postClaimText}` : ''}`;
3442
3537
  return {
3443
3538
  response: appendLegacyMcpToolWarning(toolResponse({
3444
3539
  content: [{ type: 'text', text: claimText }],
@@ -3446,6 +3541,7 @@ async function _executeMcpToolCallInner(payload) {
3446
3541
  session_id: identity.session_id,
3447
3542
  worktree_path: worktreePath,
3448
3543
  triggered_items: postClaimItems,
3544
+ ...(claimAutoSwitched ? { auto_switched: true, resolved_project: projectInfoForCwd(effectiveClaimCwd) } : {}),
3449
3545
  }), name),
3450
3546
  nextConnectionSessionId: explicitSessionIdFromEnv() ? undefined : identity.session_id,
3451
3547
  };
@@ -4232,9 +4328,9 @@ async function _executeMcpToolCallInner(payload) {
4232
4328
  }
4233
4329
  }
4234
4330
  if (name === 'bclaw_add_step') {
4235
- const crossProjectError = blockCrossProjectExecution('plan', args);
4236
- if (crossProjectError) {
4237
- return { response: crossProjectError };
4331
+ const stepLoc = resolveExecutionWriteTarget('plan', args, cwd, connectionSessionId);
4332
+ if (stepLoc.block) {
4333
+ return { response: stepLoc.block };
4238
4334
  }
4239
4335
  const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
4240
4336
  if (resolved.error) {
@@ -4256,7 +4352,7 @@ async function _executeMcpToolCallInner(payload) {
4256
4352
  return { response: createToolErrorResponse('validation_error', 'Missing required argument: planId') };
4257
4353
  if (!stepText)
4258
4354
  return { response: createToolErrorResponse('validation_error', 'Missing required argument: data.text') };
4259
- const stepTargetCwd = resolveProjectCwd(args.project, cwd);
4355
+ const stepTargetCwd = stepLoc.targetCwd;
4260
4356
  try {
4261
4357
  const result = addStepOp({ planId: stepPlanId, text: stepText, assignee: stepAssignee, estimatedEffort: stepEstimated, actualEffort: stepActual }, stepTargetCwd);
4262
4358
  return {
@@ -4277,9 +4373,9 @@ async function _executeMcpToolCallInner(payload) {
4277
4373
  }
4278
4374
  }
4279
4375
  if (name === 'bclaw_complete_step') {
4280
- const crossProjectError = blockCrossProjectExecution('plan', args);
4281
- if (crossProjectError) {
4282
- return { response: crossProjectError };
4376
+ const csLoc = resolveExecutionWriteTarget('plan', args, cwd, connectionSessionId);
4377
+ if (csLoc.block) {
4378
+ return { response: csLoc.block };
4283
4379
  }
4284
4380
  const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
4285
4381
  if (resolved.error) {
@@ -4291,7 +4387,7 @@ async function _executeMcpToolCallInner(payload) {
4291
4387
  return { response: createToolErrorResponse('validation_error', 'Missing required argument: planId') };
4292
4388
  if (!csStepId)
4293
4389
  return { response: createToolErrorResponse('validation_error', 'Missing required argument: stepId') };
4294
- const csTargetCwd = resolveProjectCwd(args.project, cwd);
4390
+ const csTargetCwd = csLoc.targetCwd;
4295
4391
  try {
4296
4392
  const result = completeStepOp({ planId: csPlanId, stepId: csStepId }, csTargetCwd);
4297
4393
  return {
@@ -4313,9 +4409,9 @@ async function _executeMcpToolCallInner(payload) {
4313
4409
  }
4314
4410
  }
4315
4411
  if (name === 'bclaw_update_step') {
4316
- const crossProjectError = blockCrossProjectExecution('plan', args);
4317
- if (crossProjectError) {
4318
- return { response: crossProjectError };
4412
+ const usLoc = resolveExecutionWriteTarget('plan', args, cwd, connectionSessionId);
4413
+ if (usLoc.block) {
4414
+ return { response: usLoc.block };
4319
4415
  }
4320
4416
  const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
4321
4417
  if (resolved.error) {
@@ -4331,7 +4427,7 @@ async function _executeMcpToolCallInner(payload) {
4331
4427
  if (args.status && !validStatuses.includes(String(args.status))) {
4332
4428
  return { response: createToolErrorResponse('validation_error', `Invalid status: ${args.status}. Valid: ${validStatuses.join(', ')}`) };
4333
4429
  }
4334
- const usTargetCwd = resolveProjectCwd(args.project, cwd);
4430
+ const usTargetCwd = usLoc.targetCwd;
4335
4431
  try {
4336
4432
  const result = updateStepOp({
4337
4433
  planId: usPlanId,
@@ -4372,9 +4468,9 @@ async function _executeMcpToolCallInner(payload) {
4372
4468
  }
4373
4469
  }
4374
4470
  if (name === 'bclaw_delete_step') {
4375
- const crossProjectError = blockCrossProjectExecution('plan', args);
4376
- if (crossProjectError) {
4377
- return { response: crossProjectError };
4471
+ const dsLoc = resolveExecutionWriteTarget('plan', args, cwd, connectionSessionId);
4472
+ if (dsLoc.block) {
4473
+ return { response: dsLoc.block };
4378
4474
  }
4379
4475
  const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
4380
4476
  if (resolved.error) {
@@ -4386,7 +4482,7 @@ async function _executeMcpToolCallInner(payload) {
4386
4482
  return { response: createToolErrorResponse('validation_error', 'Missing required argument: planId') };
4387
4483
  if (!dsStepId)
4388
4484
  return { response: createToolErrorResponse('validation_error', 'Missing required argument: stepId') };
4389
- const dsTargetCwd = resolveProjectCwd(args.project, cwd);
4485
+ const dsTargetCwd = dsLoc.targetCwd;
4390
4486
  try {
4391
4487
  const result = deleteStepOp({ planId: dsPlanId, stepId: dsStepId }, dsTargetCwd);
4392
4488
  return {
@@ -4407,10 +4503,11 @@ async function _executeMcpToolCallInner(payload) {
4407
4503
  }
4408
4504
  }
4409
4505
  if (name === 'bclaw_delete_plan') {
4410
- const crossProjectError = blockCrossProjectExecution('plan', args);
4411
- if (crossProjectError) {
4412
- return { response: crossProjectError };
4506
+ const dpLoc = resolveExecutionWriteTarget('plan', args, cwd, connectionSessionId);
4507
+ if (dpLoc.block) {
4508
+ return { response: dpLoc.block };
4413
4509
  }
4510
+ const dpTargetCwd = dpLoc.targetCwd;
4414
4511
  const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'trusted', cwd, connectionSessionId);
4415
4512
  if (resolved.error) {
4416
4513
  return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
@@ -4419,7 +4516,7 @@ async function _executeMcpToolCallInner(payload) {
4419
4516
  if (!dpId)
4420
4517
  return { response: createToolErrorResponse('validation_error', 'Missing required argument: id') };
4421
4518
  try {
4422
- const result = deletePlanOp(dpId, cwd);
4519
+ const result = deletePlanOp(dpId, dpTargetCwd);
4423
4520
  return {
4424
4521
  response: toolResponse({
4425
4522
  content: [{ type: 'text', text: `✔ Plan deleted: [${result.id}] ${result.text.slice(0, 80)}` }],
@@ -6537,15 +6634,22 @@ async function _executeMcpToolCallInner(payload) {
6537
6634
  if (name === 'bclaw_create') {
6538
6635
  try {
6539
6636
  const entity = String(args.entity ?? '');
6540
- // Execution entities stay local: the signaling-only cross-project
6541
- // boundary applies to the canonical verbs too, not just legacy tools.
6637
+ // Execution entities (plan/claim) auto-localize into a workspace sibling
6638
+ // when project=X names one: session+switch then write locally. Only
6639
+ // federated links / unknown names are blocked (signaling-only boundary).
6640
+ let targetCwd;
6641
+ let autoSwitched = false;
6542
6642
  if (entity === 'claim' || entity === 'plan') {
6543
- const crossProjectError = blockCrossProjectExecution(entity, args);
6544
- if (crossProjectError)
6545
- return { response: crossProjectError };
6643
+ const loc = resolveExecutionWriteTarget(entity, args, cwd, connectionSessionId);
6644
+ if (loc.block)
6645
+ return { response: loc.block };
6646
+ targetCwd = loc.targetCwd;
6647
+ autoSwitched = loc.autoSwitched;
6648
+ }
6649
+ else {
6650
+ targetCwd = resolveProjectCwd(args.project, cwd);
6546
6651
  }
6547
6652
  const rawData = (args.data ?? {});
6548
- const targetCwd = resolveProjectCwd(args.project, cwd);
6549
6653
  const targetScope = scopeMetadataForTarget(args, targetCwd, scopeInfo);
6550
6654
  // Auto-fill identity fields. Without this, a caller who omits author/agent
6551
6655
  // creates a schema-invalid record that is silently dropped on read and
@@ -6577,11 +6681,12 @@ async function _executeMcpToolCallInner(payload) {
6577
6681
  appendAuditEntry({ actor: actor ?? 'unknown', ...(actorId ? { actor_id: actorId } : {}), action: 'create', item_id: result.id, item_type: entity }, targetCwd);
6578
6682
  return {
6579
6683
  response: toolResponse({
6580
- content: [{ type: 'text', text: `✔ created ${entity} ${result.id}` }],
6684
+ content: [{ type: 'text', text: `✔ created ${entity} ${result.id}${autoSwitched ? ` (auto-switched → ${targetScope.resolved_project.name ?? targetScope.resolved_project.path})` : ''}` }],
6581
6685
  structuredContent: {
6582
6686
  ...result,
6583
6687
  resolved_project: targetScope.resolved_project,
6584
- active_source: targetScope.active_source,
6688
+ active_source: autoSwitched ? 'auto_switch' : targetScope.active_source,
6689
+ ...(autoSwitched ? { auto_switched: true } : {}),
6585
6690
  },
6586
6691
  }),
6587
6692
  };
@@ -6664,28 +6769,36 @@ async function _executeMcpToolCallInner(payload) {
6664
6769
  if (name === 'bclaw_transition') {
6665
6770
  try {
6666
6771
  const entity = String(args.entity ?? '');
6667
- // Same signaling-only boundary as bclaw_create: no remote lifecycle
6668
- // driving of execution entities through the canonical grammar.
6772
+ // Same auto-localize as bclaw_create: a workspace sibling named by
6773
+ // project=X is switched into and transitioned locally; only federated
6774
+ // links / unknown names are blocked (signaling-only boundary).
6775
+ let targetCwd;
6776
+ let autoSwitched = false;
6669
6777
  if (entity === 'claim' || entity === 'plan') {
6670
- const crossProjectError = blockCrossProjectExecution(entity, args);
6671
- if (crossProjectError)
6672
- return { response: crossProjectError };
6778
+ const loc = resolveExecutionWriteTarget(entity, args, cwd, connectionSessionId);
6779
+ if (loc.block)
6780
+ return { response: loc.block };
6781
+ targetCwd = loc.targetCwd;
6782
+ autoSwitched = loc.autoSwitched;
6783
+ }
6784
+ else {
6785
+ targetCwd = resolveProjectCwd(args.project, cwd);
6673
6786
  }
6674
6787
  const id = String(args.id ?? '');
6675
6788
  const to = String(args.to ?? '');
6676
6789
  const reason = args.reason;
6677
- const targetCwd = resolveProjectCwd(args.project, cwd);
6678
6790
  const targetScope = scopeMetadataForTarget(args, targetCwd, scopeInfo);
6679
6791
  const { agent_name, agent_id } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
6680
6792
  const result = transitionEntity(entity, id, to, targetCwd, reason);
6681
6793
  appendAuditEntry({ actor: agent_name, ...(agent_id ? { actor_id: agent_id } : {}), action: 'update', item_id: id, item_type: entity, reason: `transition ${result.from} → ${to}${reason ? ` (${reason})` : ''}` }, targetCwd);
6682
6794
  return {
6683
6795
  response: toolResponse({
6684
- content: [{ type: 'text', text: `✔ ${entity} ${id}: ${result.from} → ${to}` }],
6796
+ content: [{ type: 'text', text: `✔ ${entity} ${id}: ${result.from} → ${to}${autoSwitched ? ` (auto-switched → ${targetScope.resolved_project.name ?? targetScope.resolved_project.path})` : ''}` }],
6685
6797
  structuredContent: {
6686
6798
  ...result,
6687
6799
  resolved_project: targetScope.resolved_project,
6688
- active_source: targetScope.active_source,
6800
+ active_source: autoSwitched ? 'auto_switch' : targetScope.active_source,
6801
+ ...(autoSwitched ? { auto_switched: true } : {}),
6689
6802
  },
6690
6803
  }),
6691
6804
  };
package/dist/facts.js CHANGED
@@ -1,8 +1,8 @@
1
1
  // Generated by scripts/emit-site-facts.mjs at build time. Do not edit manually.
2
- // Source: brainclaw v1.11.1 on 2026-06-24T13:31:04.040Z
2
+ // Source: brainclaw v1.12.0 on 2026-06-27T13:47:04.254Z
3
3
  export const FACTS = {
4
- "version": "1.11.1",
5
- "generated_at": "2026-06-24T13:31:04.040Z",
4
+ "version": "1.12.0",
5
+ "generated_at": "2026-06-27T13:47:04.254Z",
6
6
  "tools": {
7
7
  "count": 67,
8
8
  "published_count": 66,
package/dist/facts.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "1.11.1",
3
- "generated_at": "2026-06-24T13:31:04.040Z",
2
+ "version": "1.12.0",
3
+ "generated_at": "2026-06-27T13:47:04.254Z",
4
4
  "tools": {
5
5
  "count": 67,
6
6
  "published_count": 66,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainclaw",
3
- "version": "1.11.1",
3
+ "version": "1.12.0",
4
4
  "description": "Shared project memory for humans and coding agents.",
5
5
  "type": "module",
6
6
  "repository": {