brainclaw 0.22.1 → 0.23.1
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.
- package/dist/cli.js +39 -3
- package/dist/commands/mcp.js +3 -3
- package/dist/commands/switch.js +141 -0
- package/dist/core/active-project.js +50 -0
- package/dist/core/store-resolution.js +93 -0
- package/docs/architecture/project-refs.md +19 -0
- package/docs/cli.md +47 -0
- package/docs/concepts/workspace-bootstrapping.md +41 -0
- package/docs/integrations/agents.md +35 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { Command } from 'commander';
|
|
3
4
|
import { runInit } from './commands/init.js';
|
|
4
5
|
import { runSetup } from './commands/setup.js';
|
|
@@ -78,6 +79,8 @@ import { runExplore } from './commands/explore.js';
|
|
|
78
79
|
import { getInstalledBrainclawVersion } from './core/brainclaw-version.js';
|
|
79
80
|
import { cleanOrphanFiles, memoryDir } from './core/io.js';
|
|
80
81
|
import { initLogLevel, logger } from './core/logger.js';
|
|
82
|
+
import { resolveEffectiveCwd } from './core/store-resolution.js';
|
|
83
|
+
import { runSwitch } from './commands/switch.js';
|
|
81
84
|
const program = new Command();
|
|
82
85
|
function collect(value, previous) {
|
|
83
86
|
return [...previous, value];
|
|
@@ -88,12 +91,30 @@ program
|
|
|
88
91
|
.version(getInstalledBrainclawVersion())
|
|
89
92
|
.option('--verbose', 'Show info-level log messages on stderr')
|
|
90
93
|
.option('--debug', 'Show debug-level log messages on stderr')
|
|
94
|
+
.option('--cwd <path>', 'Override working directory for this invocation')
|
|
91
95
|
.hook('preAction', (_thisCommand, actionCommand) => {
|
|
92
96
|
const root = actionCommand.optsWithGlobals();
|
|
93
97
|
initLogLevel({ verbose: root.verbose, debug: root.debug });
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
// Skip effective cwd resolution for commands that create the store
|
|
99
|
+
const cmdName = actionCommand.name();
|
|
100
|
+
const skipResolution = cmdName === 'init' || cmdName === 'setup';
|
|
101
|
+
if (!skipResolution) {
|
|
102
|
+
// Resolve effective cwd (--cwd > BRAINCLAW_PROJECT > active-project > process.cwd)
|
|
103
|
+
const effectiveCwd = resolveEffectiveCwd({ explicitCwd: root.cwd });
|
|
104
|
+
if (effectiveCwd !== process.cwd()) {
|
|
105
|
+
// Change process.cwd() so all commands resolve the correct store
|
|
106
|
+
// without needing individual --cwd plumbing
|
|
107
|
+
process.chdir(effectiveCwd);
|
|
108
|
+
logger.info(`Resolved effective cwd: ${effectiveCwd}`);
|
|
109
|
+
}
|
|
110
|
+
const removed = cleanOrphanFiles(memoryDir());
|
|
111
|
+
if (removed > 0) {
|
|
112
|
+
logger.info(`Cleaned ${removed} orphan lock/tmp file(s) in ${memoryDir()}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else if (root.cwd) {
|
|
116
|
+
// For init/setup, still respect explicit --cwd but nothing else
|
|
117
|
+
process.chdir(path.resolve(root.cwd));
|
|
97
118
|
}
|
|
98
119
|
});
|
|
99
120
|
// --- init ---
|
|
@@ -1090,6 +1111,21 @@ program
|
|
|
1090
1111
|
.action((options) => {
|
|
1091
1112
|
runExplore({ query: options.query });
|
|
1092
1113
|
});
|
|
1114
|
+
program
|
|
1115
|
+
.command('switch [project]')
|
|
1116
|
+
.description('Set the active project for subsequent commands')
|
|
1117
|
+
.option('--list', 'List available projects in the workspace')
|
|
1118
|
+
.option('--clear', 'Clear the active project (revert to cwd)')
|
|
1119
|
+
.option('--json', 'Output as JSON')
|
|
1120
|
+
.action((project, options) => {
|
|
1121
|
+
const globalOpts = options.parent?.parent ? program.opts() : {};
|
|
1122
|
+
runSwitch(project, {
|
|
1123
|
+
list: options.list,
|
|
1124
|
+
clear: options.clear,
|
|
1125
|
+
json: options.json,
|
|
1126
|
+
cwd: globalOpts.cwd,
|
|
1127
|
+
});
|
|
1128
|
+
});
|
|
1093
1129
|
program.parseAsync(process.argv).catch((err) => {
|
|
1094
1130
|
console.error(err);
|
|
1095
1131
|
process.exit(1);
|
package/dist/commands/mcp.js
CHANGED
|
@@ -29,7 +29,7 @@ import { validateMcpInput, validateMcpField } from '../core/input-validation.js'
|
|
|
29
29
|
import { buildEstimationReport } from './estimation-report.js';
|
|
30
30
|
import { detectAiAgent } from '../core/ai-agent-detection.js';
|
|
31
31
|
import { checkGitPresence, scanGitRepos, parseRoots, parseRepoSelection, parseAgentSelection, runGlobalInstall, initReposAndConfigureAgents, readSetupState, ALL_KNOWN_AGENTS, } from './setup.js';
|
|
32
|
-
import { resolveTargetStore, resolveStoreChain } from '../core/store-resolution.js';
|
|
32
|
+
import { resolveEffectiveCwd, resolveTargetStore, resolveStoreChain } from '../core/store-resolution.js';
|
|
33
33
|
import { probeForQuickSetup, buildQuickSetupProbeResponse, buildOnboardingPreview } from '../core/setup-flow.js';
|
|
34
34
|
import { ensureUserStore } from '../core/setup-state.js';
|
|
35
35
|
import { readUnseenEvents, buildNotificationSummary } from '../core/event-log.js';
|
|
@@ -888,7 +888,7 @@ export class McpServerConnection {
|
|
|
888
888
|
}
|
|
889
889
|
}
|
|
890
890
|
export function runMcp() {
|
|
891
|
-
const cwd =
|
|
891
|
+
const cwd = resolveEffectiveCwd();
|
|
892
892
|
if (!memoryExists(cwd)) {
|
|
893
893
|
console.error('Project memory not initialized. Run `brainclaw init` first.');
|
|
894
894
|
process.exit(1);
|
|
@@ -991,7 +991,7 @@ function getReviewAssignee(tags) {
|
|
|
991
991
|
return undefined;
|
|
992
992
|
}
|
|
993
993
|
export function handleMcpReadToolCall(name, args = {}, context = {}) {
|
|
994
|
-
const cwd = context.cwd ??
|
|
994
|
+
const cwd = context.cwd ?? resolveEffectiveCwd();
|
|
995
995
|
if (name === 'bclaw_get_context') {
|
|
996
996
|
const result = buildContext({
|
|
997
997
|
target: args.path,
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { loadActiveProject, saveActiveProject, clearActiveProject } from '../core/active-project.js';
|
|
3
|
+
import { memoryExists } from '../core/io.js';
|
|
4
|
+
import { resolveProjectRef, resolveWorkspaceRoot } from '../core/store-resolution.js';
|
|
5
|
+
import { scanNestedBrainclawProjects } from '../core/workspace-projects.js';
|
|
6
|
+
import { loadConfig } from '../core/config.js';
|
|
7
|
+
export function runSwitch(projectRef, options = {}) {
|
|
8
|
+
const cwd = options.cwd ?? process.cwd();
|
|
9
|
+
const wsRoot = resolveWorkspaceRoot(cwd);
|
|
10
|
+
if (!wsRoot) {
|
|
11
|
+
console.error('Error: no brainclaw workspace found. Run `brainclaw init` first.');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
// --list: show available projects
|
|
15
|
+
if (options.list) {
|
|
16
|
+
listProjects(wsRoot, options.json ?? false);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
// --clear: remove active project
|
|
20
|
+
if (options.clear) {
|
|
21
|
+
clearActiveProject(wsRoot);
|
|
22
|
+
if (options.json) {
|
|
23
|
+
console.log(JSON.stringify({ cleared: true }));
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
console.log('✔ Active project cleared. Commands will use current directory.');
|
|
27
|
+
}
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// No argument: show current active project
|
|
31
|
+
if (!projectRef) {
|
|
32
|
+
showCurrent(wsRoot, options.json ?? false);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// Switch to project
|
|
36
|
+
const resolved = resolveProjectRef(projectRef, cwd);
|
|
37
|
+
if (!resolved) {
|
|
38
|
+
console.error(`Error: cannot resolve project "${projectRef}".`);
|
|
39
|
+
console.error('Use `brainclaw switch --list` to see available projects.');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
let projectName;
|
|
43
|
+
try {
|
|
44
|
+
const config = loadConfig(resolved);
|
|
45
|
+
projectName = config.project_name;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// name is optional
|
|
49
|
+
}
|
|
50
|
+
saveActiveProject(wsRoot, {
|
|
51
|
+
path: resolved,
|
|
52
|
+
name: projectName,
|
|
53
|
+
switched_at: new Date().toISOString(),
|
|
54
|
+
switched_by: process.env.BRAINCLAW_AGENT_NAME ?? process.env.USER ?? 'unknown',
|
|
55
|
+
});
|
|
56
|
+
if (options.json) {
|
|
57
|
+
console.log(JSON.stringify({ switched: true, path: resolved, name: projectName }));
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const rel = path.relative(wsRoot, resolved) || '.';
|
|
61
|
+
console.log(`✔ Switched to ${projectName ? `"${projectName}" (${rel})` : rel}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function showCurrent(wsRoot, json) {
|
|
65
|
+
const active = loadActiveProject(wsRoot);
|
|
66
|
+
if (!active) {
|
|
67
|
+
if (json) {
|
|
68
|
+
console.log(JSON.stringify({ active: false }));
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
console.log('No active project. Commands use current directory.');
|
|
72
|
+
console.log('Use `brainclaw switch <project>` to set one.');
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const rel = path.relative(wsRoot, active.path) || '.';
|
|
77
|
+
if (json) {
|
|
78
|
+
console.log(JSON.stringify({ active: true, ...active, relative_path: rel }));
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
console.log(`Active project: ${active.name ? `"${active.name}" (${rel})` : rel}`);
|
|
82
|
+
console.log(` switched at: ${active.switched_at}`);
|
|
83
|
+
if (active.switched_by)
|
|
84
|
+
console.log(` switched by: ${active.switched_by}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function listProjects(wsRoot, json) {
|
|
88
|
+
const active = loadActiveProject(wsRoot);
|
|
89
|
+
const projects = [];
|
|
90
|
+
// Add workspace root itself
|
|
91
|
+
if (memoryExists(wsRoot)) {
|
|
92
|
+
try {
|
|
93
|
+
const config = loadConfig(wsRoot);
|
|
94
|
+
projects.push({
|
|
95
|
+
name: config.project_name,
|
|
96
|
+
path: wsRoot,
|
|
97
|
+
relative_path: '.',
|
|
98
|
+
active: active?.path === wsRoot,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
projects.push({
|
|
103
|
+
path: wsRoot,
|
|
104
|
+
relative_path: '.',
|
|
105
|
+
active: active?.path === wsRoot,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Discover child projects (depth 7 covers deep workspace layouts like /srv/dev/repos/global/applications/*/...)
|
|
110
|
+
const children = scanNestedBrainclawProjects(wsRoot, 7);
|
|
111
|
+
for (const child of children) {
|
|
112
|
+
const childPath = path.resolve(child.path);
|
|
113
|
+
if (childPath === wsRoot)
|
|
114
|
+
continue;
|
|
115
|
+
const rel = path.relative(wsRoot, childPath) || '.';
|
|
116
|
+
projects.push({
|
|
117
|
+
name: child.project_name,
|
|
118
|
+
path: childPath,
|
|
119
|
+
relative_path: rel,
|
|
120
|
+
active: active?.path === childPath,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (json) {
|
|
124
|
+
console.log(JSON.stringify({ workspace: wsRoot, projects }, null, 2));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (projects.length === 0) {
|
|
128
|
+
console.log('No brainclaw projects found in this workspace.');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
console.log(`Projects in ${wsRoot}:\n`);
|
|
132
|
+
for (const p of projects) {
|
|
133
|
+
const marker = p.active ? '→ ' : ' ';
|
|
134
|
+
const name = p.name ? `${p.name} (${p.relative_path})` : p.relative_path;
|
|
135
|
+
console.log(`${marker}${name}`);
|
|
136
|
+
}
|
|
137
|
+
if (!active) {
|
|
138
|
+
console.log('\nNo active project. Use `brainclaw switch <project>` to set one.');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=switch.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { MEMORY_DIR } from './io.js';
|
|
4
|
+
const ACTIVE_PROJECT_FILE = 'active-project.json';
|
|
5
|
+
/**
|
|
6
|
+
* Load the active project for a workspace.
|
|
7
|
+
* Returns undefined when no active project is set or the file is unreadable.
|
|
8
|
+
*/
|
|
9
|
+
export function loadActiveProject(workspaceRoot) {
|
|
10
|
+
const filePath = path.join(workspaceRoot, MEMORY_DIR, ACTIVE_PROJECT_FILE);
|
|
11
|
+
if (!fs.existsSync(filePath)) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const raw = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
16
|
+
if (typeof raw.path !== 'string' || !raw.path) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
path: raw.path,
|
|
21
|
+
name: typeof raw.name === 'string' ? raw.name : undefined,
|
|
22
|
+
switched_at: typeof raw.switched_at === 'string' ? raw.switched_at : new Date().toISOString(),
|
|
23
|
+
switched_by: typeof raw.switched_by === 'string' ? raw.switched_by : undefined,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Persist the active project for a workspace.
|
|
32
|
+
*/
|
|
33
|
+
export function saveActiveProject(workspaceRoot, project) {
|
|
34
|
+
const dir = path.join(workspaceRoot, MEMORY_DIR);
|
|
35
|
+
if (!fs.existsSync(dir)) {
|
|
36
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
const filePath = path.join(dir, ACTIVE_PROJECT_FILE);
|
|
39
|
+
fs.writeFileSync(filePath, JSON.stringify(project, null, 2) + '\n', 'utf-8');
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Clear the active project (revert to process.cwd() default).
|
|
43
|
+
*/
|
|
44
|
+
export function clearActiveProject(workspaceRoot) {
|
|
45
|
+
const filePath = path.join(workspaceRoot, MEMORY_DIR, ACTIVE_PROJECT_FILE);
|
|
46
|
+
if (fs.existsSync(filePath)) {
|
|
47
|
+
fs.unlinkSync(filePath);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=active-project.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { loadActiveProject } from './active-project.js';
|
|
4
5
|
import { loadConfig } from './config.js';
|
|
5
6
|
import { MEMORY_DIR } from './io.js';
|
|
6
7
|
import { summarizeWorkspaceProjects } from './workspace-projects.js';
|
|
@@ -84,6 +85,98 @@ export function resolveTargetStore(cwd = process.cwd(), target = 'local', option
|
|
|
84
85
|
const match = chain.find((s) => s.role === 'user');
|
|
85
86
|
return match?.cwd ?? os.homedir();
|
|
86
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Single source of truth for the effective working directory.
|
|
90
|
+
*
|
|
91
|
+
* Priority:
|
|
92
|
+
* 1. explicitCwd (--cwd flag)
|
|
93
|
+
* 2. BRAINCLAW_PROJECT env var → resolved by name/path from workspace
|
|
94
|
+
* 3. active-project.json in workspace root
|
|
95
|
+
* 4. process.cwd()
|
|
96
|
+
*/
|
|
97
|
+
export function resolveEffectiveCwd(options = {}) {
|
|
98
|
+
// 1. Explicit --cwd flag
|
|
99
|
+
if (options.explicitCwd) {
|
|
100
|
+
return path.resolve(options.explicitCwd);
|
|
101
|
+
}
|
|
102
|
+
// 2. BRAINCLAW_PROJECT env var
|
|
103
|
+
const envProject = process.env.BRAINCLAW_PROJECT;
|
|
104
|
+
if (envProject) {
|
|
105
|
+
const resolved = resolveProjectRef(envProject, process.cwd(), options.storeChainOptions);
|
|
106
|
+
if (resolved)
|
|
107
|
+
return resolved;
|
|
108
|
+
}
|
|
109
|
+
// 3. active-project.json from workspace root
|
|
110
|
+
const wsRoot = resolveWorkspaceRoot(process.cwd(), options.storeChainOptions);
|
|
111
|
+
if (wsRoot) {
|
|
112
|
+
const active = loadActiveProject(wsRoot);
|
|
113
|
+
if (active && fs.existsSync(path.join(active.path, MEMORY_DIR, 'config.yaml'))) {
|
|
114
|
+
return active.path;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// 4. Default
|
|
118
|
+
return process.cwd();
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Find the workspace root (farthest store in the chain, or the one with
|
|
122
|
+
* role=workspace). Returns undefined when no store exists.
|
|
123
|
+
*/
|
|
124
|
+
export function resolveWorkspaceRoot(cwd = process.cwd(), options = {}) {
|
|
125
|
+
const chain = resolveStoreChain(cwd, options);
|
|
126
|
+
if (chain.length === 0)
|
|
127
|
+
return undefined;
|
|
128
|
+
const ws = chain.find((s) => s.role === 'workspace');
|
|
129
|
+
return ws?.cwd ?? chain[chain.length - 1].cwd;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Resolve a project reference (name or relative path) to an absolute path.
|
|
133
|
+
* Returns undefined when the reference cannot be resolved to a valid brainclaw project.
|
|
134
|
+
*/
|
|
135
|
+
export function resolveProjectRef(ref, cwd = process.cwd(), storeChainOptions) {
|
|
136
|
+
const wsRoot = resolveWorkspaceRoot(cwd, storeChainOptions);
|
|
137
|
+
if (!wsRoot)
|
|
138
|
+
return undefined;
|
|
139
|
+
// Try as absolute path
|
|
140
|
+
if (path.isAbsolute(ref)) {
|
|
141
|
+
return fs.existsSync(path.join(ref, MEMORY_DIR, 'config.yaml')) ? ref : undefined;
|
|
142
|
+
}
|
|
143
|
+
// Try as relative path from workspace root
|
|
144
|
+
const asPath = path.resolve(wsRoot, ref);
|
|
145
|
+
if (fs.existsSync(path.join(asPath, MEMORY_DIR, 'config.yaml'))) {
|
|
146
|
+
return asPath;
|
|
147
|
+
}
|
|
148
|
+
// Try by project name: scan child stores for matching project_name
|
|
149
|
+
const chain = resolveStoreChain(wsRoot, storeChainOptions);
|
|
150
|
+
for (const store of chain) {
|
|
151
|
+
if (store.cwd === wsRoot)
|
|
152
|
+
continue; // skip workspace itself
|
|
153
|
+
try {
|
|
154
|
+
const config = loadConfig(store.cwd);
|
|
155
|
+
if (config.project_name === ref)
|
|
156
|
+
return store.cwd;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// skip unreadable configs
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Try discovering child projects by scanning filesystem
|
|
163
|
+
try {
|
|
164
|
+
const wsConfig = loadConfig(wsRoot);
|
|
165
|
+
const summary = summarizeWorkspaceProjects(wsRoot, wsConfig);
|
|
166
|
+
for (const project of summary.discovered_projects) {
|
|
167
|
+
const projectPath = path.resolve(wsRoot, project.path);
|
|
168
|
+
if (project.project_name === ref) {
|
|
169
|
+
if (fs.existsSync(path.join(projectPath, MEMORY_DIR, 'config.yaml'))) {
|
|
170
|
+
return projectPath;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// fall through
|
|
177
|
+
}
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
87
180
|
/**
|
|
88
181
|
* Resolve the most specific child store that should answer a context request.
|
|
89
182
|
*
|
|
@@ -257,10 +257,29 @@ Likely persisted fields:
|
|
|
257
257
|
|
|
258
258
|
This should stay clearly separate from canonical memory items such as decisions and constraints.
|
|
259
259
|
|
|
260
|
+
## Current Implementation (v0.23.0)
|
|
261
|
+
|
|
262
|
+
`brainclaw switch` provides the first layer of project navigation:
|
|
263
|
+
|
|
264
|
+
- `brainclaw switch <name-or-path>` — set active project by name or relative path
|
|
265
|
+
- `brainclaw switch --list` — discover available projects in workspace
|
|
266
|
+
- `resolveProjectRef()` — shared resolver for CLI and MCP (name, path, registry lookup)
|
|
267
|
+
- `resolveEffectiveCwd()` — single source of truth: `--cwd` > `BRAINCLAW_PROJECT` env > active-project > cwd
|
|
268
|
+
- MCP tools automatically resolve the active project
|
|
269
|
+
|
|
270
|
+
This covers the most common agent friction (cwd-dependent resolution) without requiring the full `project_ref` model yet.
|
|
271
|
+
|
|
260
272
|
## Migration Strategy
|
|
261
273
|
|
|
262
274
|
The migration should be low-risk and incremental.
|
|
263
275
|
|
|
276
|
+
Phase 0 (done):
|
|
277
|
+
|
|
278
|
+
- `brainclaw switch` with name/path resolution
|
|
279
|
+
- global `--cwd` option
|
|
280
|
+
- `BRAINCLAW_PROJECT` environment variable
|
|
281
|
+
- `resolveEffectiveCwd()` used by CLI and MCP
|
|
282
|
+
|
|
264
283
|
Phase 1:
|
|
265
284
|
|
|
266
285
|
- introduce `project_ref` in workspace discovery/registry
|
package/docs/cli.md
CHANGED
|
@@ -11,6 +11,53 @@ For capable coding agents, prefer MCP for dynamic runtime state:
|
|
|
11
11
|
|
|
12
12
|
Use the CLI when a human operator is driving the workflow, when you are scripting setup or release operations, or when MCP is not the integration path.
|
|
13
13
|
|
|
14
|
+
## Global Options
|
|
15
|
+
|
|
16
|
+
All commands support these global options:
|
|
17
|
+
|
|
18
|
+
| Option | Description |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `--cwd <path>` | Override working directory for this invocation. Bypasses active project and env var resolution. |
|
|
21
|
+
| `--verbose` | Show info-level log messages on stderr |
|
|
22
|
+
| `--debug` | Show debug-level log messages on stderr |
|
|
23
|
+
|
|
24
|
+
**Effective cwd resolution priority** (highest wins):
|
|
25
|
+
|
|
26
|
+
1. `--cwd` flag
|
|
27
|
+
2. `BRAINCLAW_PROJECT` environment variable (project name or path)
|
|
28
|
+
3. Active project set via `brainclaw switch`
|
|
29
|
+
4. `process.cwd()` (shell working directory)
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Multi-Project Navigation
|
|
34
|
+
|
|
35
|
+
### `brainclaw switch [project]`
|
|
36
|
+
|
|
37
|
+
Set the active project for subsequent CLI and MCP commands. This eliminates the need to `cd` into a subproject directory in multi-project workspaces. The active project is persisted per-workspace in `.brainclaw/active-project.json`.
|
|
38
|
+
|
|
39
|
+
| Option | Description |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `--list` | List all available projects in the workspace |
|
|
42
|
+
| `--clear` | Clear the active project (revert to cwd default) |
|
|
43
|
+
| `--json` | Output as JSON |
|
|
44
|
+
|
|
45
|
+
The `<project>` argument accepts:
|
|
46
|
+
- **Project name** — matched against the global registry and workspace config
|
|
47
|
+
- **Relative path** — resolved from the workspace root (e.g. `apps/lodestar`)
|
|
48
|
+
- **Absolute path** — used directly
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
brainclaw switch --list # discover available projects
|
|
52
|
+
brainclaw switch lodestar # switch by project name
|
|
53
|
+
brainclaw switch apps/lodestar # switch by relative path
|
|
54
|
+
brainclaw switch # show current active project
|
|
55
|
+
brainclaw switch --clear # clear, revert to cwd
|
|
56
|
+
brainclaw --cwd /other/path status # one-off override without switching
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**MCP usage:** The active project also affects MCP tools. When `bclaw_get_context()` is called without an explicit path, it resolves context from the active project's store. Agents can also use `BRAINCLAW_PROJECT=<name>` environment variable for the same effect.
|
|
60
|
+
|
|
14
61
|
---
|
|
15
62
|
|
|
16
63
|
## Initialize and Inspect
|
|
@@ -38,3 +38,44 @@ If shared memory is absent, that should not always be interpreted as "there is n
|
|
|
38
38
|
It may simply mean the workspace has not been onboarded yet.
|
|
39
39
|
|
|
40
40
|
This lets a single machine support multiple very different workspaces without forcing one static instruction layer to fit all of them equally well.
|
|
41
|
+
|
|
42
|
+
## Multi-project workspaces
|
|
43
|
+
|
|
44
|
+
A workspace may contain multiple brainclaw-initialized child projects (each with its own `.brainclaw/` store). In this topology:
|
|
45
|
+
|
|
46
|
+
- The workspace root holds shared instructions, constraints, and coordination state
|
|
47
|
+
- Each child project holds project-specific memory (decisions, traps, plans)
|
|
48
|
+
- The store chain walks upward: child → repo → workspace → user
|
|
49
|
+
|
|
50
|
+
### Working with child projects
|
|
51
|
+
|
|
52
|
+
Agents and operators can address child projects without `cd`:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
brainclaw switch apps/lodestar # set active project
|
|
56
|
+
brainclaw plan list # now targets lodestar's store
|
|
57
|
+
brainclaw switch --clear # back to workspace root
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Or use environment variables:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
export BRAINCLAW_PROJECT=lodestar
|
|
64
|
+
brainclaw context # resolves lodestar's store
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Or one-off overrides:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
brainclaw --cwd apps/lodestar plan list
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Project discovery
|
|
74
|
+
|
|
75
|
+
`brainclaw switch --list` discovers child projects via:
|
|
76
|
+
|
|
77
|
+
1. Global project registry
|
|
78
|
+
2. Workspace config `projects.known`
|
|
79
|
+
3. Filesystem scan for subdirectories containing `.brainclaw/`
|
|
80
|
+
|
|
81
|
+
The bootstrap analysis (`analyzeRepository`) also detects brainclaw-native workspace complexity (child stores, folder strategy, known projects) alongside classic monorepo markers.
|
|
@@ -101,6 +101,41 @@ brainclaw adds all generated files to `.gitignore` automatically during init.
|
|
|
101
101
|
|
|
102
102
|
Exception: use `--shared` if you intentionally want the main instruction file (e.g., CLAUDE.md) versioned for the whole team.
|
|
103
103
|
|
|
104
|
+
## Multi-project workspaces
|
|
105
|
+
|
|
106
|
+
When a workspace contains multiple brainclaw-initialized child projects, agents need to target the correct project store. There are three mechanisms (from most to least ergonomic):
|
|
107
|
+
|
|
108
|
+
### 1. `brainclaw switch` (persistent)
|
|
109
|
+
|
|
110
|
+
An operator or agent sets the active project once, and all subsequent commands resolve against it:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
brainclaw switch apps/lodestar # set active project
|
|
114
|
+
brainclaw plan list # targets lodestar
|
|
115
|
+
bclaw_get_context() # MCP also targets lodestar
|
|
116
|
+
brainclaw switch --clear # back to workspace root
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 2. `BRAINCLAW_PROJECT` environment variable
|
|
120
|
+
|
|
121
|
+
Set in the shell or agent configuration. Useful for CI/CD or when the agent can control its environment:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
export BRAINCLAW_PROJECT=lodestar
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 3. `--cwd` flag (one-off override)
|
|
128
|
+
|
|
129
|
+
For a single command without changing the active project:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
brainclaw --cwd apps/lodestar plan list
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Priority**: `--cwd` > `BRAINCLAW_PROJECT` > active project > shell cwd.
|
|
136
|
+
|
|
137
|
+
**Discovery**: use `brainclaw switch --list` to see all available projects in the workspace.
|
|
138
|
+
|
|
104
139
|
## Session lifecycle
|
|
105
140
|
|
|
106
141
|
### Starting work
|