brainclaw 1.5.3 → 1.5.5

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.
@@ -305,6 +305,91 @@ export function printReloadReminder(detectedAgent) {
305
305
  console.log(' → Restart your AI coding agent to pick up the new MCP configuration.');
306
306
  }
307
307
  }
308
+ function logDetectedAgentSurfaces(detectedName, detectedSurfaces) {
309
+ console.log('');
310
+ if (detectedName) {
311
+ console.log(`Detected AI agent: ${detectedName}`);
312
+ }
313
+ else {
314
+ console.log('No AI agent detected automatically.');
315
+ }
316
+ const visibleSurfaces = detectedSurfaces.filter((surface) => surface.status !== 'not_detected');
317
+ if (visibleSurfaces.length > 0) {
318
+ console.log('Other AI work surfaces on this machine:');
319
+ for (const surface of visibleSurfaces) {
320
+ console.log(` - ${surface.display_name} [${surface.surface_kind}, ${surface.status}]`);
321
+ }
322
+ const usageHints = renderAiSurfaceUsageHints(visibleSurfaces);
323
+ if (usageHints.length > 0) {
324
+ console.log('');
325
+ console.log('Suggested uses:');
326
+ for (const line of usageHints) {
327
+ console.log(` ${line}`);
328
+ }
329
+ }
330
+ console.log('');
331
+ console.log('These surfaces are tracked separately from coding agents and will use tailored onboarding flows.');
332
+ }
333
+ }
334
+ async function resolveSelectedAgentsForSetup(options, detectedName) {
335
+ console.log('Supported agents:');
336
+ ALL_KNOWN_AGENTS.forEach((a, i) => {
337
+ const tag = a === detectedName ? ' ← detected' : '';
338
+ console.log(` ${i + 1}) ${a}${tag}`);
339
+ });
340
+ let agentChoice;
341
+ if (options.agents) {
342
+ agentChoice = options.agents;
343
+ }
344
+ else if (options.yes || !process.stdin.isTTY) {
345
+ agentChoice = detectedName ? 'detected' : 'all';
346
+ }
347
+ else {
348
+ const defaultChoice = detectedName ? 'detected' : 'all';
349
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
350
+ try {
351
+ agentChoice = (await rl.question(`Configure agents: (d)etected, (a)ll, or numbers e.g. 1,3 [${defaultChoice}]: `)).trim() || defaultChoice;
352
+ }
353
+ finally {
354
+ rl.close();
355
+ }
356
+ }
357
+ const selectedAgents = parseAgentSelection(agentChoice, detectedName);
358
+ console.log(`Selected agents: ${selectedAgents.length === 0 ? '(none)' : selectedAgents.join(', ')}`);
359
+ return selectedAgents;
360
+ }
361
+ export async function runSetupMachine(options = {}) {
362
+ const env = process.env;
363
+ const detectedAi = detectAiAgent(env);
364
+ const detectedName = detectedAi?.name;
365
+ const testMode = process.env.BRAINCLAW_TEST_MODE === '1';
366
+ const detectedSurfaces = testMode ? [] : buildAiSurfaceInventory();
367
+ console.log(BRAINCLAW_ASCII);
368
+ console.log('Machine bootstrap only — no repositories will be scanned or initialized.');
369
+ logDetectedAgentSurfaces(detectedName, detectedSurfaces);
370
+ const selectedAgents = await resolveSelectedAgentsForSetup(options, detectedName);
371
+ console.log('\n→ Installing machine-level brainclaw prerequisites...');
372
+ const written = runGlobalInstall(selectedAgents, env);
373
+ if (written.length > 0) {
374
+ for (const filePath of written) {
375
+ console.log(` ✔ ${filePath}`);
376
+ }
377
+ }
378
+ else {
379
+ console.log(' (all machine-level prerequisites already up to date)');
380
+ }
381
+ installVscodeExtension();
382
+ writeSetupState({
383
+ completed_at: new Date().toISOString(),
384
+ roots: [],
385
+ initialised_repos: [],
386
+ global_configs_written: selectedAgents,
387
+ }, env);
388
+ printReloadReminder(detectedName);
389
+ console.log('');
390
+ console.log('Next step: run `brainclaw init` inside the project you want to create or refresh.');
391
+ console.log('If the project already has .brainclaw/ and you want to register another agent explicitly, run `brainclaw enable-agent <agent-name>` there.');
392
+ }
308
393
  // ─── Main CLI wizard ──────────────────────────────────────────────────────────
309
394
  export async function runSetup(options = {}) {
310
395
  const env = process.env;
@@ -399,54 +484,8 @@ export async function runSetup(options = {}) {
399
484
  const detectedName = detectedAi?.name;
400
485
  const testMode = process.env.BRAINCLAW_TEST_MODE === '1';
401
486
  const detectedSurfaces = testMode ? [] : buildAiSurfaceInventory();
402
- console.log('');
403
- if (detectedName) {
404
- console.log(`Detected AI agent: ${detectedName}`);
405
- }
406
- else {
407
- console.log('No AI agent detected automatically.');
408
- }
409
- const visibleSurfaces = detectedSurfaces.filter((surface) => surface.status !== 'not_detected');
410
- if (visibleSurfaces.length > 0) {
411
- console.log('Other AI work surfaces on this machine:');
412
- for (const surface of visibleSurfaces) {
413
- console.log(` - ${surface.display_name} [${surface.surface_kind}, ${surface.status}]`);
414
- }
415
- const usageHints = renderAiSurfaceUsageHints(visibleSurfaces);
416
- if (usageHints.length > 0) {
417
- console.log('');
418
- console.log('Suggested uses:');
419
- for (const line of usageHints) {
420
- console.log(` ${line}`);
421
- }
422
- }
423
- console.log('');
424
- console.log('These surfaces are tracked separately from coding agents and will use tailored onboarding flows.');
425
- }
426
- console.log('Supported agents:');
427
- ALL_KNOWN_AGENTS.forEach((a, i) => {
428
- const tag = a === detectedName ? ' ← detected' : '';
429
- console.log(` ${i + 1}) ${a}${tag}`);
430
- });
431
- let agentChoice;
432
- if (options.agents) {
433
- agentChoice = options.agents;
434
- }
435
- else if (options.yes || !process.stdin.isTTY) {
436
- agentChoice = detectedName ? 'detected' : 'all';
437
- }
438
- else {
439
- const defaultChoice = detectedName ? 'detected' : 'all';
440
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
441
- try {
442
- agentChoice = (await rl.question(`Configure agents: (d)etected, (a)ll, or numbers e.g. 1,3 [${defaultChoice}]: `)).trim() || defaultChoice;
443
- }
444
- finally {
445
- rl.close();
446
- }
447
- }
448
- const selectedAgents = parseAgentSelection(agentChoice, detectedName);
449
- console.log(`Selected agents: ${selectedAgents.length === 0 ? '(none)' : selectedAgents.join(', ')}`);
487
+ logDetectedAgentSurfaces(detectedName, detectedSurfaces);
488
+ const selectedAgents = await resolveSelectedAgentsForSetup(options, detectedName);
450
489
  // Step 5: Global install
451
490
  console.log('\n→ Installing global brainclaw prerequisites...');
452
491
  const written = runGlobalInstall(selectedAgents, env);
@@ -244,6 +244,10 @@ function listProjects(wsRoot, json) {
244
244
  * this returns the farthest one — the true multi-project workspace root.
245
245
  */
246
246
  function findOutermostWorkspaceRoot(startDir) {
247
+ const envWorkspace = process.env.BRAINCLAW_CWD?.trim();
248
+ if (envWorkspace && memoryExists(path.resolve(envWorkspace))) {
249
+ return path.resolve(envWorkspace);
250
+ }
247
251
  let dir = path.resolve(startDir);
248
252
  const root = path.parse(dir).root;
249
253
  const home = process.env.HOME || process.env.USERPROFILE || root;
@@ -320,6 +320,16 @@ export function syncAgentRunFromAssignmentTransition(assignment, newStatus, opti
320
320
  artifacts: options.artifacts,
321
321
  }, cwd);
322
322
  return;
323
+ case 'cancelled':
324
+ if (!canTransitionRun(run, 'cancelled'))
325
+ return;
326
+ transitionAgentRun(run.id, 'cancelled', {
327
+ actor: options.actor,
328
+ actor_id: options.actor_id,
329
+ session_id: options.session_id,
330
+ status_reason: options.status_reason ?? 'Cancelled via assignment lifecycle',
331
+ }, cwd);
332
+ return;
323
333
  case 'failed':
324
334
  if (!canTransitionRun(run, 'failed'))
325
335
  return;
@@ -64,6 +64,22 @@ export function listAssignments(cwd, filter) {
64
64
  items = items.filter((a) => a.sequence_id === filter.sequence_id);
65
65
  return items;
66
66
  }
67
+ export function deleteAssignment(id, cwd) {
68
+ const store = assignmentStore(cwd);
69
+ if (!store.exists(id)) {
70
+ return false;
71
+ }
72
+ mutate({ cwd }, () => {
73
+ const writableStore = new JsonStore({
74
+ dirPath: assignmentsDir(cwd, 'write'),
75
+ documentType: 'assignment',
76
+ getId: (a) => a.id,
77
+ sort: (a, b) => a.created_at.localeCompare(b.created_at),
78
+ });
79
+ writableStore.delete(id);
80
+ });
81
+ return true;
82
+ }
67
83
  // ── ID Generation ────────────────────────────────────────────
68
84
  export function generateAssignmentId(cwd) {
69
85
  return generateIdWithLabel('assignments', cwd);
@@ -78,15 +94,15 @@ export function generateAssignmentId(cwd) {
78
94
  * rerouted a still-unstarted lane.
79
95
  */
80
96
  const VALID_TRANSITIONS = new Map([
81
- ['created', new Set(['offered', 'rerouted'])],
82
- ['offered', new Set(['accepted', 'failed', 'expired', 'rerouted'])],
83
- ['accepted', new Set(['started', 'timed_out', 'rerouted'])],
84
- ['started', new Set(['completed', 'failed', 'blocked', 'timed_out', 'rerouted'])],
85
- ['failed', new Set(['retrying', 'rerouted'])],
86
- ['timed_out', new Set(['retrying', 'rerouted'])],
87
- ['retrying', new Set(['offered', 'rerouted'])],
88
- ['blocked', new Set(['rerouted', 'started', 'failed'])],
89
- // Terminal: completed, expired, rerouted (no outgoing transitions)
97
+ ['created', new Set(['offered', 'rerouted', 'cancelled'])],
98
+ ['offered', new Set(['accepted', 'failed', 'expired', 'rerouted', 'cancelled'])],
99
+ ['accepted', new Set(['started', 'timed_out', 'rerouted', 'cancelled'])],
100
+ ['started', new Set(['completed', 'failed', 'blocked', 'timed_out', 'rerouted', 'cancelled'])],
101
+ ['failed', new Set(['retrying', 'rerouted', 'cancelled'])],
102
+ ['timed_out', new Set(['retrying', 'rerouted', 'cancelled'])],
103
+ ['retrying', new Set(['offered', 'rerouted', 'cancelled'])],
104
+ ['blocked', new Set(['rerouted', 'started', 'failed', 'cancelled'])],
105
+ // Terminal: completed, cancelled, expired, rerouted (no outgoing transitions)
90
106
  ]);
91
107
  export function validateTransition(from, to) {
92
108
  const allowed = VALID_TRANSITIONS.get(from);
@@ -148,6 +164,9 @@ export function transitionAssignment(id, newStatus, options, cwd) {
148
164
  case 'completed':
149
165
  assignment.completed_at = now;
150
166
  break;
167
+ case 'cancelled':
168
+ assignment.cancelled_at = now;
169
+ break;
151
170
  case 'failed':
152
171
  assignment.failed_at = now;
153
172
  break;
@@ -280,7 +299,7 @@ export function createAssignment(options, cwd) {
280
299
  }
281
300
  // ── Active Assignment Lookup ─────────────────────────────────
282
301
  /** Statuses that indicate a finished assignment (no longer active). */
283
- const TERMINAL_STATUSES = new Set(['completed', 'expired', 'rerouted']);
302
+ const TERMINAL_STATUSES = new Set(['completed', 'cancelled', 'expired', 'rerouted']);
284
303
  /**
285
304
  * Return the most recently created non-terminal assignment for the given agent.
286
305
  * When `claimId` is provided, it is used as a fast-path lookup before falling
@@ -445,7 +445,7 @@ export function buildContext(options = {}) {
445
445
  const currentSession = loadCurrentSession(contextCwd);
446
446
  if (currentAgentIdentity || agent) {
447
447
  const claimPlanIds = new Set(myClaims.map((c) => c.plan_id).filter(Boolean));
448
- const activeAssignments = listAssignments(contextCwd, { agent: agentName }).filter((assignment) => !['completed', 'failed', 'expired', 'rerouted'].includes(assignment.status));
448
+ const activeAssignments = listAssignments(contextCwd, { agent: agentName }).filter((assignment) => !['completed', 'failed', 'cancelled', 'expired', 'rerouted'].includes(assignment.status));
449
449
  const inProgressPlans = state.plan_items.filter((p) => p.status === 'in_progress' &&
450
450
  (p.assignee === agentName || claimPlanIds.has(p.id)));
451
451
  if (myClaims.length > 0 || activeAssignments.length > 0 || inProgressPlans.length > 0) {
@@ -88,7 +88,7 @@ export function buildCoordinationSnapshot(options = {}) {
88
88
  : filteredClaims,
89
89
  active_assignments: (agent
90
90
  ? listAssignments(options.cwd, { agent })
91
- : listAssignments(options.cwd)).filter((assignment) => !['completed', 'failed', 'expired', 'rerouted'].includes(assignment.status) &&
91
+ : listAssignments(options.cwd)).filter((assignment) => !['completed', 'failed', 'cancelled', 'expired', 'rerouted'].includes(assignment.status) &&
92
92
  (!project || !assignment.plan_id || filteredPlans.some((plan) => plan.id === assignment.plan_id))),
93
93
  active_runs: (agent
94
94
  ? listAgentRuns(options.cwd, { agent })
@@ -17,7 +17,7 @@ import { archiveCandidate, listCandidates, loadCandidate, saveCandidate, } from
17
17
  import { addCrossProjectLink, removeCrossProjectLink, resolveCrossProjectLinks, } from './cross-project.js';
18
18
  import { listClaims } from './claims.js';
19
19
  import { listActionRequired } from './actions.js';
20
- import { listAssignments } from './assignments.js';
20
+ import { deleteAssignment, listAssignments, loadAssignment, saveAssignment, transitionAssignment } from './assignments.js';
21
21
  import { listAgentRuns } from './agentruns.js';
22
22
  import { deleteRuntimeNote, listRuntimeNotes, saveRuntimeNote, } from './runtime.js';
23
23
  import { createConstraint, createDecision, createTrap, } from './operations/memory-write.js';
@@ -309,6 +309,14 @@ export function updateEntity(name, id, patch, cwd) {
309
309
  saveRuntimeNote(patched, cwd);
310
310
  return { entity: name, id };
311
311
  }
312
+ case 'assignment': {
313
+ const assignment = loadAssignment(id, cwd);
314
+ if (!assignment)
315
+ throw new EntityNotFoundError(name, id);
316
+ const patched = { ...assignment, ...patch };
317
+ saveAssignment(patched, cwd);
318
+ return { entity: name, id };
319
+ }
312
320
  case 'candidate': {
313
321
  const candidate = loadCandidate(id, cwd);
314
322
  const patched = { ...candidate, ...patch };
@@ -371,6 +379,28 @@ export function removeEntity(name, id, cwd, purge = false) {
371
379
  const removed = removeCrossProjectLink(id, cwd);
372
380
  return { entity: name, id: removed.name ?? removed.path, archived: false, purged: true };
373
381
  }
382
+ case 'assignment': {
383
+ const assignment = loadAssignment(id, cwd);
384
+ if (!assignment)
385
+ throw new EntityNotFoundError(name, id);
386
+ if (purge) {
387
+ const deleted = deleteAssignment(id, cwd);
388
+ if (!deleted)
389
+ throw new EntityNotFoundError(name, id);
390
+ return { entity: name, id, archived: false, purged: true };
391
+ }
392
+ if (assignment.status === 'cancelled') {
393
+ return { entity: name, id, archived: true, purged: false };
394
+ }
395
+ if (ENTITY_REGISTRY.assignment.terminal.includes(assignment.status)) {
396
+ throw new Error(`assignment '${id}' is already terminal (${assignment.status}); use purge:true to hard-delete if needed`);
397
+ }
398
+ transitionAssignment(id, 'cancelled', {
399
+ actor: 'brainclaw',
400
+ status_reason: 'Archived via bclaw_remove',
401
+ }, cwd);
402
+ return { entity: name, id, archived: true, purged: false };
403
+ }
374
404
  default:
375
405
  throw new EntityOperationUnsupportedError(name, 'remove');
376
406
  }
@@ -418,6 +448,13 @@ export function transitionEntity(name, id, to, cwd, _reason) {
418
448
  }
419
449
  throw new InvalidTransitionError(name, from, to);
420
450
  }
451
+ case 'assignment': {
452
+ transitionAssignment(id, to, {
453
+ actor: 'brainclaw',
454
+ status_reason: _reason,
455
+ }, cwd);
456
+ return { entity: name, id, from, to, side_effects: sideEffects };
457
+ }
421
458
  default:
422
459
  throw new EntityOperationUnsupportedError(name, 'transition', `Lifecycle transitions for ${name} not yet wired.`);
423
460
  }
@@ -234,24 +234,25 @@ const assignment = {
234
234
  name: 'assignment',
235
235
  shortLabelPrefix: 'asgn',
236
236
  schema: AssignmentSchema,
237
- updatable: ['brief', 'tags'],
237
+ updatable: ['description', 'status_reason', 'tags'],
238
238
  statusField: 'status',
239
239
  transitions: {
240
- created: ['offered', 'expired'],
241
- offered: ['accepted', 'expired', 'rerouted'],
242
- accepted: ['started', 'failed', 'timed_out'],
243
- started: ['completed', 'failed', 'blocked', 'timed_out'],
244
- failed: ['retrying'],
245
- timed_out: ['retrying'],
246
- retrying: ['offered'],
247
- blocked: ['started', 'rerouted', 'failed'],
240
+ created: ['offered', 'expired', 'rerouted', 'cancelled'],
241
+ offered: ['accepted', 'expired', 'rerouted', 'failed', 'cancelled'],
242
+ accepted: ['started', 'failed', 'timed_out', 'rerouted', 'cancelled'],
243
+ started: ['completed', 'failed', 'blocked', 'timed_out', 'rerouted', 'cancelled'],
244
+ failed: ['retrying', 'rerouted', 'cancelled'],
245
+ timed_out: ['retrying', 'rerouted', 'cancelled'],
246
+ retrying: ['offered', 'rerouted', 'cancelled'],
247
+ blocked: ['started', 'rerouted', 'failed', 'cancelled'],
248
248
  },
249
- terminal: ['completed', 'expired', 'rerouted'],
249
+ terminal: ['completed', 'cancelled', 'expired', 'rerouted'],
250
250
  sideEffects: {
251
251
  'created->offered': ['timestamp:offered_at', 'event:assignment_offered', 'audit:assignment_offered'],
252
252
  'offered->accepted': ['timestamp:accepted_at', 'event:assignment_accepted', 'sync:agent_run'],
253
253
  'accepted->started': ['timestamp:started_at', 'event:assignment_started', 'sync:agent_run'],
254
254
  'started->completed': ['timestamp:completed_at', 'event:assignment_completed', 'sync:agent_run'],
255
+ 'started->cancelled': ['timestamp:cancelled_at', 'event:assignment_cancelled', 'sync:agent_run'],
255
256
  'started->failed': ['timestamp:failed_at', 'event:assignment_failed', 'sync:agent_run'],
256
257
  },
257
258
  };
@@ -598,6 +598,7 @@ export const AssignmentStatusSchema = z.enum([
598
598
  'accepted', // Worker acknowledged receipt
599
599
  'started', // Worker reports active work begun
600
600
  'completed', // Worker reports successful completion
601
+ 'cancelled', // Supervisor/admin aborted the assignment explicitly
601
602
  'failed', // Worker reports failure
602
603
  'blocked', // Worker reports external blocker
603
604
  'timed_out', // Sweeper detected no heartbeat within TTL
@@ -642,6 +643,7 @@ export const AssignmentSchema = z.object({
642
643
  accepted_at: z.string().optional(),
643
644
  started_at: z.string().optional(),
644
645
  completed_at: z.string().optional(),
646
+ cancelled_at: z.string().optional(),
645
647
  failed_at: z.string().optional(),
646
648
  blocked_at: z.string().optional(),
647
649
  timed_out_at: z.string().optional(),
@@ -814,6 +816,7 @@ export const RuntimeEventTypeSchema = z.enum([
814
816
  'assignment_started',
815
817
  'assignment_progress',
816
818
  'assignment_completed',
819
+ 'assignment_cancelled',
817
820
  'assignment_failed',
818
821
  'assignment_blocked',
819
822
  'assignment_timed_out',
@@ -91,48 +91,53 @@ export function resolveTargetStore(cwd = process.cwd(), target = 'local', option
91
91
  *
92
92
  * Priority:
93
93
  * 1. explicitCwd (--cwd flag)
94
- * 2. BRAINCLAW_CWD env var → absolute workspace path injected by MCP configs
95
- * 3. BRAINCLAW_PROJECT env var → resolved by name/path from workspace
96
- * 4. Session-scoped active project (from .current-session)
94
+ * 2. BRAINCLAW_CWD env var → workspace anchor injected by MCP configs
95
+ * 3. BRAINCLAW_PROJECT env var → resolved by name/path from workspace anchor
96
+ * 4. Session-scoped active project (from .current-session under the anchor)
97
97
  * 5. Global active-project.json in workspace root
98
- * 6. process.cwd()
98
+ * 6. Workspace anchor or process.cwd()
99
99
  */
100
100
  export function resolveEffectiveCwd(options = {}) {
101
+ const baseCwd = path.resolve(options.baseCwd ?? process.cwd());
101
102
  // 1. Explicit --cwd flag
102
103
  if (options.explicitCwd) {
103
104
  return path.resolve(options.explicitCwd);
104
105
  }
105
- // 2. BRAINCLAW_CWD env var — set by MCP configs to bind to the workspace
106
- // regardless of the IDE's process.cwd() at launch time
106
+ // 2. BRAINCLAW_CWD env var — set by MCP configs to anchor resolution to the
107
+ // workspace regardless of the IDE's process.cwd() at launch time. It is a
108
+ // workspace anchor, not the final answer: session/global active project
109
+ // state still overrides it.
110
+ let anchorCwd = baseCwd;
107
111
  const envCwd = process.env.BRAINCLAW_CWD?.trim();
108
- if (envCwd && fs.existsSync(path.join(path.resolve(envCwd), MEMORY_DIR, 'config.yaml'))) {
109
- return path.resolve(envCwd);
112
+ const hasEnvWorkspace = !!envCwd && fs.existsSync(path.join(path.resolve(envCwd), MEMORY_DIR, 'config.yaml'));
113
+ if (hasEnvWorkspace) {
114
+ anchorCwd = path.resolve(envCwd);
110
115
  }
111
116
  // 3. BRAINCLAW_PROJECT env var
112
117
  const envProject = process.env.BRAINCLAW_PROJECT;
113
118
  if (envProject) {
114
- const resolved = resolveProjectRef(envProject, process.cwd(), options.storeChainOptions);
119
+ const resolved = resolveProjectRef(envProject, anchorCwd, options.storeChainOptions);
115
120
  if (resolved)
116
121
  return resolved;
117
122
  }
118
- // 3. Session-scoped active project (per-agent, no cross-agent interference)
119
- const session = loadCurrentSession(process.cwd());
123
+ // 4. Session-scoped active project (per-agent, no cross-agent interference)
124
+ const session = loadCurrentSession(anchorCwd);
120
125
  if (session?.active_project) {
121
126
  const sp = session.active_project;
122
127
  if (fs.existsSync(path.join(sp.path, MEMORY_DIR, 'config.yaml'))) {
123
128
  return sp.path;
124
129
  }
125
130
  }
126
- // 4. Global active-project.json from workspace root
127
- const wsRoot = resolveWorkspaceRoot(process.cwd(), options.storeChainOptions);
131
+ // 5. Global active-project.json from workspace root
132
+ const wsRoot = hasEnvWorkspace ? anchorCwd : resolveWorkspaceRoot(anchorCwd, options.storeChainOptions);
128
133
  if (wsRoot) {
129
134
  const active = loadActiveProject(wsRoot);
130
135
  if (active && fs.existsSync(path.join(active.path, MEMORY_DIR, 'config.yaml'))) {
131
136
  return active.path;
132
137
  }
133
138
  }
134
- // 5. Default
135
- return process.cwd();
139
+ // 6. Default
140
+ return anchorCwd;
136
141
  }
137
142
  /**
138
143
  * Find the workspace root (farthest store in the chain, or the one with
@@ -150,9 +155,14 @@ export function resolveWorkspaceRoot(cwd = process.cwd(), options = {}) {
150
155
  * Returns undefined when the reference cannot be resolved to a valid brainclaw project.
151
156
  */
152
157
  export function resolveProjectRef(ref, cwd = process.cwd(), storeChainOptions) {
158
+ const envWorkspace = process.env.BRAINCLAW_CWD?.trim();
159
+ const workspaceAnchor = envWorkspace && fs.existsSync(path.join(path.resolve(envWorkspace), MEMORY_DIR, 'config.yaml'))
160
+ ? path.resolve(envWorkspace)
161
+ : undefined;
153
162
  // Walk UP from real cwd to find the outermost .brainclaw/ — this avoids
154
163
  // circular resolution when an active project narrows the workspace view.
155
- const wsRoot = findOutermostBrainclawRoot(process.cwd())
164
+ const wsRoot = workspaceAnchor
165
+ ?? findOutermostBrainclawRoot(cwd)
156
166
  ?? resolveWorkspaceRoot(cwd, storeChainOptions);
157
167
  if (!wsRoot)
158
168
  return undefined;
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.5.3 on 2026-05-10T19:05:14.160Z
2
+ // Source: brainclaw v1.5.5 on 2026-05-13T19:24:16.549Z
3
3
  export const FACTS = {
4
- "version": "1.5.3",
5
- "generated_at": "2026-05-10T19:05:14.160Z",
4
+ "version": "1.5.5",
5
+ "generated_at": "2026-05-13T19:24:16.549Z",
6
6
  "tools": {
7
7
  "count": 60,
8
8
  "published_count": 59,
package/dist/facts.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "1.5.3",
3
- "generated_at": "2026-05-10T19:05:14.160Z",
2
+ "version": "1.5.5",
3
+ "generated_at": "2026-05-13T19:24:16.549Z",
4
4
  "tools": {
5
5
  "count": 60,
6
6
  "published_count": 59,