brainclaw 1.9.1 → 1.10.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/README.md +78 -25
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +18 -1
- package/dist/commands/code-map.js +129 -0
- package/dist/commands/codev.js +7 -0
- package/dist/commands/dispatch-watch.js +1 -1
- package/dist/commands/doctor.js +3 -5
- package/dist/commands/loops-handlers.js +4 -1
- package/dist/commands/mcp-read-handlers.js +8 -0
- package/dist/commands/mcp.js +121 -1
- package/dist/commands/metrics.js +0 -1
- package/dist/commands/release-claims.js +1 -1
- package/dist/commands/run-profile.js +3 -2
- package/dist/commands/sequence.js +1 -1
- package/dist/commands/switch.js +100 -89
- package/dist/commands/sync.js +1 -1
- package/dist/commands/upgrade.js +0 -7
- package/dist/core/agent-context.js +1 -1
- package/dist/core/agent-files.js +13 -2
- package/dist/core/agent-integrations.js +3 -3
- package/dist/core/agent-registry.js +2 -2
- package/dist/core/assignments.js +12 -0
- package/dist/core/brainclaw-version.js +2 -2
- package/dist/core/code-map/backend.js +176 -0
- package/dist/core/code-map/core.js +81 -0
- package/dist/core/code-map/drafts.js +2 -0
- package/dist/core/code-map/extractor.js +29 -0
- package/dist/core/code-map/finalizer.js +191 -0
- package/dist/core/code-map/freshness.js +144 -0
- package/dist/core/code-map/ids.js +0 -0
- package/dist/core/code-map/importable.js +35 -0
- package/dist/core/code-map/indexes.js +197 -0
- package/dist/core/code-map/lang/java/imports.scm +17 -0
- package/dist/core/code-map/lang/java/index.js +254 -0
- package/dist/core/code-map/lang/java/tags.scm +48 -0
- package/dist/core/code-map/lang/php/imports.scm +21 -0
- package/dist/core/code-map/lang/php/index.js +251 -0
- package/dist/core/code-map/lang/php/tags.scm +44 -0
- package/dist/core/code-map/lang/provider.js +9 -0
- package/dist/core/code-map/lang/providers.js +24 -0
- package/dist/core/code-map/lang/python/imports.scm +90 -0
- package/dist/core/code-map/lang/python/index.js +364 -0
- package/dist/core/code-map/lang/python/tags.scm +81 -0
- package/dist/core/code-map/lang/query-runtime.js +374 -0
- package/dist/core/code-map/lang/registry.js +125 -0
- package/dist/core/code-map/lang/typescript/imports.scm +90 -0
- package/dist/core/code-map/lang/typescript/index.js +306 -0
- package/dist/core/code-map/lang/typescript/tags.js.scm +106 -0
- package/dist/core/code-map/lang/typescript/tags.scm +151 -0
- package/dist/core/code-map/lock.js +210 -0
- package/dist/core/code-map/materialized.js +51 -0
- package/dist/core/code-map/memory-reader.js +59 -0
- package/dist/core/code-map/paths.js +53 -0
- package/dist/core/code-map/query.js +599 -0
- package/dist/core/code-map/refresh.js +0 -0
- package/dist/core/code-map/resolve.js +177 -0
- package/dist/core/code-map/store.js +206 -0
- package/dist/core/code-map/types.js +293 -0
- package/dist/core/code-map/vocabulary.js +57 -0
- package/dist/core/code-map/wasm-loader.js +294 -0
- package/dist/core/code-map/work-section.js +206 -0
- package/dist/core/codev-rounds.js +4 -0
- package/dist/core/context.js +1 -1
- package/dist/core/cross-project.js +1 -1
- package/dist/core/dispatcher.js +0 -2
- package/dist/core/entity-operations.js +0 -3
- package/dist/core/execution-adapters.js +11 -10
- package/dist/core/execution-profile.js +58 -0
- package/dist/core/facade-schema.js +9 -0
- package/dist/core/ids.js +1 -1
- package/dist/core/instruction-templates.js +2 -0
- package/dist/core/instructions.js +0 -1
- package/dist/core/loops/lock.js +0 -3
- package/dist/core/mcp-command-resolution.js +3 -1
- package/dist/core/protocol-skills.js +5 -3
- package/dist/core/security-detectors.js +2 -2
- package/dist/core/security-extract.js +2 -2
- package/dist/core/store-resolution.js +41 -4
- package/dist/facts.js +9 -5
- package/dist/facts.json +8 -4
- package/dist/vendor/web-tree-sitter/tree-sitter.js +3980 -0
- package/dist/vendor/web-tree-sitter/tree-sitter.wasm +0 -0
- package/dist/wasm/tree-sitter-java.wasm +0 -0
- package/dist/wasm/tree-sitter-javascript.wasm +0 -0
- package/dist/wasm/tree-sitter-php.wasm +0 -0
- package/dist/wasm/tree-sitter-python.wasm +0 -0
- package/dist/wasm/tree-sitter-tsx.wasm +0 -0
- package/dist/wasm/tree-sitter-typescript.wasm +0 -0
- package/dist/wasm/tree-sitter.wasm +0 -0
- package/docs/cli.md +46 -8
- package/docs/code-map.md +209 -0
- package/docs/integrations/mcp.md +13 -6
- package/docs/mcp-schema-changelog.md +7 -3
- package/docs/quickstart.md +1 -1
- package/package.json +11 -6
|
@@ -37,8 +37,9 @@ export function runRunProfile(profileName, options = {}) {
|
|
|
37
37
|
console.error(`Unknown agent: ${options.agent}. Using profile invoke template.`);
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
// Replace {prompt} placeholder with the profile prompt
|
|
41
|
-
|
|
40
|
+
// Replace {prompt} placeholder with the profile prompt. Escape backslashes
|
|
41
|
+
// before quotes so a backslash in the prompt can't break out of the quoting.
|
|
42
|
+
const command = invoke.replace(/\{prompt\}/g, profile.prompt.replace(/\\/g, '\\\\').replace(/"/g, '\\"'));
|
|
42
43
|
if (options.dry) {
|
|
43
44
|
console.log(`[dry-run] Profile: ${profile.name}`);
|
|
44
45
|
console.log(`[dry-run] Command: ${command}`);
|
|
@@ -11,7 +11,7 @@ function parseItems(raw) {
|
|
|
11
11
|
parsed = JSON.parse(raw);
|
|
12
12
|
}
|
|
13
13
|
catch (error) {
|
|
14
|
-
throw new Error(`Invalid --items JSON: ${error instanceof Error ? error.message : String(error)}
|
|
14
|
+
throw new Error(`Invalid --items JSON: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
15
15
|
}
|
|
16
16
|
if (!Array.isArray(parsed)) {
|
|
17
17
|
throw new Error('Invalid --items JSON: expected an array');
|
package/dist/commands/switch.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { loadActiveProject, saveActiveProject, clearActiveProject } from '../core/active-project.js';
|
|
3
|
-
import { buildOperationalIdentity, loadCurrentSession, loadSessionById, saveCurrentSession } from '../core/identity.js';
|
|
3
|
+
import { buildOperationalIdentity, loadCurrentSession, loadSessionById, resolveCurrentSessionId, saveCurrentSession } from '../core/identity.js';
|
|
4
4
|
import { memoryExists } from '../core/io.js';
|
|
5
5
|
import { resolveProjectRef } from '../core/store-resolution.js';
|
|
6
6
|
import { resolveCrossProjectLinks, resolveProjectCwd } from '../core/cross-project.js';
|
|
@@ -160,59 +160,70 @@ export function runSwitch(projectRef, options = {}) {
|
|
|
160
160
|
}
|
|
161
161
|
// --list: show available projects
|
|
162
162
|
if (options.list) {
|
|
163
|
-
listProjects(wsRoot, options.json ?? false);
|
|
163
|
+
listProjects(wsRoot, cwd, options.json ?? false);
|
|
164
164
|
return;
|
|
165
165
|
}
|
|
166
|
-
// --clear: remove active project
|
|
166
|
+
// --clear: remove active project. Session-scoped by default (F3) — clearing
|
|
167
|
+
// the SHARED global pointer is an opt-in (--global) so one agent's clear no
|
|
168
|
+
// longer wipes every other agent's resolution.
|
|
167
169
|
if (options.clear) {
|
|
168
|
-
|
|
169
|
-
if (
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
let scope;
|
|
171
|
+
if (options.global) {
|
|
172
|
+
clearActiveProject(wsRoot);
|
|
173
|
+
scope = 'global';
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
const session = loadCurrentSession(cwd);
|
|
177
|
+
if (session?.active_project) {
|
|
178
|
+
const { active_project: _removed, ...rest } = session;
|
|
179
|
+
saveCurrentSession(rest, cwd);
|
|
180
|
+
}
|
|
181
|
+
scope = 'session';
|
|
172
182
|
}
|
|
173
|
-
clearActiveProject(wsRoot);
|
|
174
183
|
if (options.json) {
|
|
175
|
-
console.log(JSON.stringify({ cleared: true }));
|
|
184
|
+
console.log(JSON.stringify({ cleared: true, scope }));
|
|
176
185
|
}
|
|
177
186
|
else {
|
|
178
|
-
|
|
187
|
+
const hint = scope === 'session' ? ' (session-scoped)' : ' (global)';
|
|
188
|
+
console.log(`✔ Active project cleared${hint}. Commands will use current directory.`);
|
|
179
189
|
}
|
|
180
190
|
return;
|
|
181
191
|
}
|
|
182
192
|
// No argument: show current active project
|
|
183
193
|
if (!projectRef) {
|
|
184
|
-
showCurrent(wsRoot, options.json ?? false);
|
|
194
|
+
showCurrent(wsRoot, cwd, options.json ?? false);
|
|
185
195
|
return;
|
|
186
196
|
}
|
|
187
197
|
// Switch to project
|
|
188
|
-
const resolved = resolveProjectRef(projectRef, cwd);
|
|
189
|
-
if (!resolved) {
|
|
190
|
-
console.error(`Error: cannot resolve project "${projectRef}".`);
|
|
191
|
-
console.error('Use `brainclaw switch --list` to see available projects.');
|
|
192
|
-
process.exit(1);
|
|
193
|
-
}
|
|
194
|
-
let projectName;
|
|
195
|
-
try {
|
|
196
|
-
const config = loadConfig(resolved);
|
|
197
|
-
projectName = config.project_name;
|
|
198
|
-
}
|
|
199
|
-
catch {
|
|
200
|
-
// name is optional
|
|
201
|
-
}
|
|
202
198
|
const now = new Date().toISOString();
|
|
203
|
-
const session = loadCurrentSession(cwd);
|
|
204
|
-
const scopedToSession = options.session ?? !!session;
|
|
205
199
|
let scope;
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
200
|
+
let switchedPath;
|
|
201
|
+
let switchedName;
|
|
202
|
+
if (options.global) {
|
|
203
|
+
// Opt-in, audited: set the SHARED workspace default for every agent on the
|
|
204
|
+
// host. Bypasses the session entirely (an operator setting a default).
|
|
205
|
+
// Resolve store-chain children AND cross-project links (mirror switchProject)
|
|
206
|
+
// so `switch <linked> --global` matches what --list shows and what the
|
|
207
|
+
// session path can target (Codex final review F3-F5 finding).
|
|
208
|
+
let resolved = resolveProjectRef(projectRef, cwd);
|
|
209
|
+
if (!resolved) {
|
|
210
|
+
try {
|
|
211
|
+
const linkResolved = resolveProjectCwd(projectRef, cwd);
|
|
212
|
+
if (linkResolved !== cwd)
|
|
213
|
+
resolved = linkResolved;
|
|
214
|
+
}
|
|
215
|
+
catch { /* falls through to the error below */ }
|
|
216
|
+
}
|
|
217
|
+
if (!resolved) {
|
|
218
|
+
console.error(`Error: cannot resolve project "${projectRef}".`);
|
|
219
|
+
console.error('Use `brainclaw switch --list` to see available projects.');
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
let projectName;
|
|
223
|
+
try {
|
|
224
|
+
projectName = loadConfig(resolved).project_name;
|
|
225
|
+
}
|
|
226
|
+
catch { /* name is optional */ }
|
|
216
227
|
saveActiveProject(wsRoot, {
|
|
217
228
|
path: resolved,
|
|
218
229
|
name: projectName,
|
|
@@ -220,21 +231,47 @@ export function runSwitch(projectRef, options = {}) {
|
|
|
220
231
|
switched_by: process.env.BRAINCLAW_AGENT_NAME ?? process.env.USER ?? 'unknown',
|
|
221
232
|
});
|
|
222
233
|
scope = 'global';
|
|
234
|
+
switchedPath = resolved;
|
|
235
|
+
switchedName = projectName;
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
// F3 default: session-scoped + isolated. Delegate to switchProject — the
|
|
239
|
+
// safe model that auto-creates the session, honours an explicit
|
|
240
|
+
// BRAINCLAW_SESSION_ID (resolveCurrentSessionId returns it WITHOUT
|
|
241
|
+
// persisting, so the session file must be created), resolves cross-project
|
|
242
|
+
// links, and never touches the shared global pointer.
|
|
243
|
+
try {
|
|
244
|
+
const explicitSessionId = resolveCurrentSessionId(process.env, cwd) || undefined;
|
|
245
|
+
const result = switchProject(projectRef, { cwd, sessionOnly: true, sessionId: explicitSessionId });
|
|
246
|
+
scope = 'session';
|
|
247
|
+
switchedPath = result.path;
|
|
248
|
+
switchedName = result.name;
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
console.error(`Error: ${err.message}`);
|
|
252
|
+
console.error('Use `brainclaw switch --list` to see available projects.');
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
223
255
|
}
|
|
224
256
|
if (options.json) {
|
|
225
|
-
console.log(JSON.stringify({ switched: true, path:
|
|
257
|
+
console.log(JSON.stringify({ switched: true, path: switchedPath, name: switchedName, scope }));
|
|
226
258
|
}
|
|
227
259
|
else {
|
|
228
|
-
const rel = path.relative(wsRoot,
|
|
229
|
-
const scopeHint = scope === 'session' ? ' (session-scoped)' : '';
|
|
230
|
-
console.log(`✔ Switched to ${
|
|
260
|
+
const rel = path.relative(wsRoot, switchedPath) || '.';
|
|
261
|
+
const scopeHint = scope === 'session' ? ' (session-scoped)' : ' (global — all agents)';
|
|
262
|
+
console.log(`✔ Switched to ${switchedName ? `"${switchedName}" (${rel})` : rel}${scopeHint}`);
|
|
231
263
|
}
|
|
232
264
|
}
|
|
233
|
-
function showCurrent(wsRoot, json) {
|
|
234
|
-
|
|
265
|
+
function showCurrent(wsRoot, cwd, json) {
|
|
266
|
+
// F5: prefer the session's own active project so an agent sees its own
|
|
267
|
+
// session-scoped switch, not just the shared global pointer.
|
|
268
|
+
const sessionActive = loadCurrentSession(cwd)?.active_project;
|
|
269
|
+
const globalActive = loadActiveProject(wsRoot);
|
|
270
|
+
const active = sessionActive ?? globalActive;
|
|
271
|
+
const source = sessionActive ? 'session' : globalActive ? 'global' : 'none';
|
|
235
272
|
if (!active) {
|
|
236
273
|
if (json) {
|
|
237
|
-
console.log(JSON.stringify({ active: false }));
|
|
274
|
+
console.log(JSON.stringify({ active: false, scope: 'none' }));
|
|
238
275
|
}
|
|
239
276
|
else {
|
|
240
277
|
console.log('No active project. Commands use current directory.');
|
|
@@ -243,67 +280,41 @@ function showCurrent(wsRoot, json) {
|
|
|
243
280
|
return;
|
|
244
281
|
}
|
|
245
282
|
const rel = path.relative(wsRoot, active.path) || '.';
|
|
283
|
+
const switchedBy = 'switched_by' in active ? active.switched_by : undefined;
|
|
246
284
|
if (json) {
|
|
247
|
-
console.log(JSON.stringify({ active: true, ...active, relative_path: rel }));
|
|
285
|
+
console.log(JSON.stringify({ active: true, ...active, relative_path: rel, scope: source }));
|
|
248
286
|
}
|
|
249
287
|
else {
|
|
250
|
-
|
|
288
|
+
const scopeHint = source === 'session' ? ' (session-scoped)' : ' (global — all agents)';
|
|
289
|
+
console.log(`Active project: ${active.name ? `"${active.name}" (${rel})` : rel}${scopeHint}`);
|
|
251
290
|
console.log(` switched at: ${active.switched_at}`);
|
|
252
|
-
if (
|
|
253
|
-
console.log(` switched by: ${
|
|
291
|
+
if (switchedBy)
|
|
292
|
+
console.log(` switched by: ${switchedBy}`);
|
|
254
293
|
}
|
|
255
294
|
}
|
|
256
|
-
function listProjects(wsRoot, json) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (memoryExists(wsRoot)) {
|
|
261
|
-
try {
|
|
262
|
-
const config = loadConfig(wsRoot);
|
|
263
|
-
projects.push({
|
|
264
|
-
name: config.project_name,
|
|
265
|
-
path: wsRoot,
|
|
266
|
-
relative_path: '.',
|
|
267
|
-
active: active?.path === wsRoot,
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
catch {
|
|
271
|
-
projects.push({
|
|
272
|
-
path: wsRoot,
|
|
273
|
-
relative_path: '.',
|
|
274
|
-
active: active?.path === wsRoot,
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
// Discover child projects (depth 7 covers deep workspace layouts like /srv/dev/repos/global/applications/*/...)
|
|
279
|
-
const children = scanNestedBrainclawProjects(wsRoot, 7);
|
|
280
|
-
for (const child of children) {
|
|
281
|
-
const childPath = path.resolve(child.path);
|
|
282
|
-
if (childPath === wsRoot)
|
|
283
|
-
continue;
|
|
284
|
-
const rel = path.relative(wsRoot, childPath) || '.';
|
|
285
|
-
projects.push({
|
|
286
|
-
name: child.project_name,
|
|
287
|
-
path: childPath,
|
|
288
|
-
relative_path: rel,
|
|
289
|
-
active: active?.path === childPath,
|
|
290
|
-
});
|
|
291
|
-
}
|
|
295
|
+
function listProjects(wsRoot, cwd, json) {
|
|
296
|
+
// F5: delegate to the session-aware lister so the active marker reflects the
|
|
297
|
+
// agent's own session active project, falling back to the global pointer.
|
|
298
|
+
const result = listAvailableProjectsForSession(cwd);
|
|
292
299
|
if (json) {
|
|
293
|
-
console.log(JSON.stringify({
|
|
300
|
+
console.log(JSON.stringify({
|
|
301
|
+
workspace: result.workspace_root,
|
|
302
|
+
active_source: result.active_source,
|
|
303
|
+
projects: result.projects,
|
|
304
|
+
}, null, 2));
|
|
294
305
|
return;
|
|
295
306
|
}
|
|
296
|
-
if (projects.length === 0) {
|
|
307
|
+
if (result.projects.length === 0) {
|
|
297
308
|
console.log('No brainclaw projects found in this workspace.');
|
|
298
309
|
return;
|
|
299
310
|
}
|
|
300
|
-
console.log(`Projects in ${
|
|
301
|
-
for (const p of projects) {
|
|
311
|
+
console.log(`Projects in ${result.workspace_root}:\n`);
|
|
312
|
+
for (const p of result.projects) {
|
|
302
313
|
const marker = p.active ? '→ ' : ' ';
|
|
303
314
|
const name = p.name ? `${p.name} (${p.relative_path})` : p.relative_path;
|
|
304
315
|
console.log(`${marker}${name}`);
|
|
305
316
|
}
|
|
306
|
-
if (
|
|
317
|
+
if (result.active_source === 'none') {
|
|
307
318
|
console.log('\nNo active project. Use `brainclaw switch <project>` to set one.');
|
|
308
319
|
}
|
|
309
320
|
}
|
package/dist/commands/sync.js
CHANGED
|
@@ -61,7 +61,7 @@ export function runSync(options = {}) {
|
|
|
61
61
|
return;
|
|
62
62
|
}
|
|
63
63
|
// Check git status of .brainclaw/
|
|
64
|
-
let gitStatus
|
|
64
|
+
let gitStatus;
|
|
65
65
|
try {
|
|
66
66
|
// Security: execFileSync (no shell) + scopePaths spread as separate args so
|
|
67
67
|
// path specs cannot inject (Socket 2026-06-08 class).
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -463,13 +463,6 @@ function upsertSection(existingContent, section) {
|
|
|
463
463
|
const trimmed = existingContent.trimEnd();
|
|
464
464
|
return trimmed.length > 0 ? `${trimmed}\n\n${section}\n` : `${section}\n`;
|
|
465
465
|
}
|
|
466
|
-
function listJsonFiles(dir) {
|
|
467
|
-
if (!fs.existsSync(dir))
|
|
468
|
-
return [];
|
|
469
|
-
return fs.readdirSync(dir)
|
|
470
|
-
.filter(f => f.endsWith('.json'))
|
|
471
|
-
.map(f => path.join(dir, f));
|
|
472
|
-
}
|
|
473
466
|
function listJsonFilesRecursive(dir) {
|
|
474
467
|
if (!fs.existsSync(dir))
|
|
475
468
|
return [];
|
|
@@ -63,7 +63,7 @@ function readAgentsMarkdown(cwd) {
|
|
|
63
63
|
// Only extract rules from actionable sections, not from descriptive sections
|
|
64
64
|
// like "why this matters" which contain explanatory bullets, not instructions.
|
|
65
65
|
const SKIP_SECTIONS = /why this matters|what it provides|what brainclaw/i;
|
|
66
|
-
let currentSection
|
|
66
|
+
let currentSection;
|
|
67
67
|
let skipSection = false;
|
|
68
68
|
const rules = [];
|
|
69
69
|
for (const line of lines) {
|
package/dist/core/agent-files.js
CHANGED
|
@@ -24,6 +24,18 @@ This project uses brainclaw for shared coordination between humans and agents.
|
|
|
24
24
|
2. Check **Your open work** for active claims and in-progress plans assigned to you
|
|
25
25
|
3. Respect active claims from other agents — check \`brainclaw claim list\` before editing a claimed scope
|
|
26
26
|
|
|
27
|
+
### Before editing unfamiliar code (Code Map)
|
|
28
|
+
|
|
29
|
+
Don't grep the repo blind. Orient with the Code Map first:
|
|
30
|
+
|
|
31
|
+
\`\`\`bash
|
|
32
|
+
brainclaw code-map brief <symbol-or-path> # ranked reading list + related decisions/traps (MCP: bclaw_code_brief)
|
|
33
|
+
brainclaw code-map find <name> # locate a symbol/class/component (MCP: bclaw_code_find)
|
|
34
|
+
brainclaw code-map status # freshness
|
|
35
|
+
brainclaw code-map refresh --all # when status is missing_index
|
|
36
|
+
brainclaw code-map refresh --changed # when status is stale_*
|
|
37
|
+
\`\`\`
|
|
38
|
+
|
|
27
39
|
### Before finishing (required)
|
|
28
40
|
|
|
29
41
|
1. Release claims you opened: \`brainclaw claim release <id>\` — or \`brainclaw session-end --auto-release\`
|
|
@@ -1146,7 +1158,7 @@ export function ensureClaudeCodeCommand(cwd) {
|
|
|
1146
1158
|
relativePath: CLAUDE_CODE_COMMAND_RELATIVE_PATH,
|
|
1147
1159
|
};
|
|
1148
1160
|
}
|
|
1149
|
-
export function ensureClaudeCodeUserSettings(homeDir,
|
|
1161
|
+
export function ensureClaudeCodeUserSettings(homeDir, _env = process.env) {
|
|
1150
1162
|
if (!homeDir)
|
|
1151
1163
|
return undefined;
|
|
1152
1164
|
const filePath = path.join(homeDir, '.claude', 'settings.json');
|
|
@@ -1618,7 +1630,6 @@ export function ensureCodexMcpConfig(homeDir, env = process.env) {
|
|
|
1618
1630
|
const autoApprovedSet = new Set(getHeadlessAutoApprovedToolNames());
|
|
1619
1631
|
const toolSectionRe = /^\[mcp_servers\.brainclaw\.tools\.([^\]]+)\]/gm;
|
|
1620
1632
|
const approvalModeRe = /^\s*approval_mode\s*=\s*"([^"]+)"/m;
|
|
1621
|
-
let m;
|
|
1622
1633
|
const warnings = [];
|
|
1623
1634
|
// Split into sections to check each tool block
|
|
1624
1635
|
const lines = existing.split('\n');
|
|
@@ -160,8 +160,8 @@ export function extractMcpCommandVal(agentName, expectedPath) {
|
|
|
160
160
|
return { is_valid: false };
|
|
161
161
|
}
|
|
162
162
|
if (expectedPath.endsWith('.toml')) {
|
|
163
|
-
const cmdMatch = content.match(/\[mcp_servers\.brainclaw\](?:[
|
|
164
|
-
const argsMatch = content.match(/\[mcp_servers\.brainclaw\](?:[
|
|
163
|
+
const cmdMatch = content.match(/\[mcp_servers\.brainclaw\](?:[^[]*)command\s*=\s*(["'])(.+?)\1/is);
|
|
164
|
+
const argsMatch = content.match(/\[mcp_servers\.brainclaw\](?:[^[]*)args\s*=\s*\[(.+?)\]/is);
|
|
165
165
|
let args;
|
|
166
166
|
if (argsMatch) {
|
|
167
167
|
args = argsMatch[1]
|
|
@@ -274,7 +274,7 @@ export function assessAgentIntegrationReadiness(config, cwd, env = process.env)
|
|
|
274
274
|
const surfaces = declaration.surfaces.map((surface) => surfaceExists(surface, cwd, env, declaration.agent_name));
|
|
275
275
|
const missingSurfaces = surfaces.filter((surface) => !surface.exists);
|
|
276
276
|
const driftingSurfaces = surfaces.filter((surface) => surface.drift_message != null);
|
|
277
|
-
let effectiveTier
|
|
277
|
+
let effectiveTier;
|
|
278
278
|
const selfHealingGuidance = [];
|
|
279
279
|
const hasMissingMcpOrHook = missingSurfaces.some((s) => s.kind === 'mcp' || s.kind === 'hook');
|
|
280
280
|
const hasDriftingMcp = driftingSurfaces.some((s) => s.kind === 'mcp');
|
|
@@ -275,7 +275,7 @@ export function registerAgentIdentity(input) {
|
|
|
275
275
|
saveAgentIdentity(created, input.cwd, input.preferredDirName);
|
|
276
276
|
return created;
|
|
277
277
|
}
|
|
278
|
-
export function resolveCurrentAgentIdentity(cwd, preferredDirName,
|
|
278
|
+
export function resolveCurrentAgentIdentity(cwd, preferredDirName, _homeDir) {
|
|
279
279
|
// env var takes priority over config — allows AI agent to self-identify
|
|
280
280
|
const envAgentId = (process.env.BRAINCLAW_AGENT_ID ?? '').trim();
|
|
281
281
|
const envAgentName = (process.env.BRAINCLAW_AGENT_NAME ?? process.env.BRAINCLAW_AGENT ?? '').trim();
|
|
@@ -537,7 +537,7 @@ export function resolveCurrentModel(cwd) {
|
|
|
537
537
|
* Note: config.current_agent is intentionally NOT used here — it's a singleton
|
|
538
538
|
* global that causes cross-agent confusion in multi-agent setups.
|
|
539
539
|
*/
|
|
540
|
-
export function resolveCurrentAgentName(
|
|
540
|
+
export function resolveCurrentAgentName(_cwd, _homeDir) {
|
|
541
541
|
const fromEnv = (process.env.BRAINCLAW_AGENT_NAME ?? process.env.BRAINCLAW_AGENT)?.trim();
|
|
542
542
|
if (fromEnv)
|
|
543
543
|
return fromEnv;
|
package/dist/core/assignments.js
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assignment lifecycle — Agent SDK runtime protocol.
|
|
3
|
+
*
|
|
4
|
+
* An Assignment is the canonical coordination entity that tracks the full
|
|
5
|
+
* lifecycle of dispatched work: from creation through offer, acceptance,
|
|
6
|
+
* execution, and completion (or failure/timeout/reroute).
|
|
7
|
+
*
|
|
8
|
+
* Assignments reference Claims (scope lock) and InboxMessages (brief delivery)
|
|
9
|
+
* but don't replace them. They own the status FSM and heartbeat tracking.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
1
13
|
import fs from 'node:fs';
|
|
2
14
|
import { AssignmentSchema } from './schema.js';
|
|
3
15
|
import { resolveEntityDir } from './io.js';
|
|
@@ -555,7 +555,7 @@ function readWorkspaceBrainclawPackage(cwd) {
|
|
|
555
555
|
}
|
|
556
556
|
catch (error) {
|
|
557
557
|
const message = error instanceof Error ? error.message : String(error);
|
|
558
|
-
throw new Error(`Failed to read package.json: ${message}
|
|
558
|
+
throw new Error(`Failed to read package.json: ${message}`, { cause: error });
|
|
559
559
|
}
|
|
560
560
|
const name = typeof parsed.name === 'string' ? parsed.name.trim() : '';
|
|
561
561
|
const version = typeof parsed.version === 'string' ? parsed.version.trim() : '';
|
|
@@ -617,7 +617,7 @@ function parseNpmDistTags(stdout) {
|
|
|
617
617
|
}
|
|
618
618
|
catch (error) {
|
|
619
619
|
const message = error instanceof Error ? error.message : String(error);
|
|
620
|
-
throw new Error(`npm view returned invalid JSON: ${message}
|
|
620
|
+
throw new Error(`npm view returned invalid JSON: ${message}`, { cause: error });
|
|
621
621
|
}
|
|
622
622
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
623
623
|
throw new Error('npm view did not return a dist-tag object.');
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeQueryBackend — the agent-facing query contract (spec §8).
|
|
3
|
+
*
|
|
4
|
+
* Introduced in P0 so a future Memgraph (or other) backend can be added without
|
|
5
|
+
* changing the agent-facing APIs. P0 ships exactly one implementation:
|
|
6
|
+
* `JsonlBackend`. In this sprint, `status()` and `refresh()` are minimally real
|
|
7
|
+
* (they read/init the durable store and report freshness); `find()`/`brief()`
|
|
8
|
+
* return not-yet-implemented placeholders that still carry a real
|
|
9
|
+
* `freshness_badge`, locking the response shape for later sprints.
|
|
10
|
+
*/
|
|
11
|
+
import { execFileSync } from 'node:child_process';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { readManifest, storeExists } from './store.js';
|
|
14
|
+
import { refresh as runRefresh } from './refresh.js';
|
|
15
|
+
import { applyGitHeadDrift } from './freshness.js';
|
|
16
|
+
import { brief as runBrief, find as runFind } from './query.js';
|
|
17
|
+
import { defaultMemoryReader } from './memory-reader.js';
|
|
18
|
+
/** spec §9 caps the brief reading list at 12 files. */
|
|
19
|
+
export const BRIEF_FILE_CAP = 12;
|
|
20
|
+
function badge(status, details = {}) {
|
|
21
|
+
return { status, details };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Read the working tree's current commit at `root` (read-path git-HEAD drift,
|
|
25
|
+
* trp_42688015). Returns null on any failure or a non-git project (also detached
|
|
26
|
+
* HEAD resolves to the commit sha, which is the correct comparison key) — a null
|
|
27
|
+
* makes the comparison a no-op, preserving existing behaviour.
|
|
28
|
+
*
|
|
29
|
+
* COST (review finding, LOW): one synchronous `git rev-parse HEAD` per status/
|
|
30
|
+
* find/brief call. These are interactive, human-/agent-paced reads (not a tight
|
|
31
|
+
* loop), so a single ~5–15ms spawn is acceptable and keeps branch-switch detection
|
|
32
|
+
* immediate. If this ever shows up on a profile, memoize per `root` behind a short
|
|
33
|
+
* TTL (a few seconds) — short enough that a checkout is still caught promptly.
|
|
34
|
+
*/
|
|
35
|
+
function readCurrentGitHead(root) {
|
|
36
|
+
try {
|
|
37
|
+
const out = execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
38
|
+
cwd: root,
|
|
39
|
+
encoding: 'utf-8',
|
|
40
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
41
|
+
}).trim();
|
|
42
|
+
return out || null;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* P0 JSONL-backed query backend. Reads the durable file store (manifest +
|
|
50
|
+
* shards + indexes); no graph DB. find()/brief() are stubbed for Sprint 1.
|
|
51
|
+
*/
|
|
52
|
+
export class JsonlBackend {
|
|
53
|
+
/**
|
|
54
|
+
* Related-memory read seam (spec §11). Defaults to the canonical entity read
|
|
55
|
+
* path; tests inject an in-memory reader to assert attachment without a store.
|
|
56
|
+
*/
|
|
57
|
+
memoryReader;
|
|
58
|
+
/** Read-path git-HEAD reader (injectable for tests). trp_42688015. */
|
|
59
|
+
gitHeadReader;
|
|
60
|
+
constructor(opts = {}) {
|
|
61
|
+
this.memoryReader = opts.memoryReader ?? defaultMemoryReader;
|
|
62
|
+
this.gitHeadReader = opts.gitHeadReader ?? readCurrentGitHead;
|
|
63
|
+
}
|
|
64
|
+
async status(input) {
|
|
65
|
+
const manifest = readManifest(input.cwd, input.preferredDirName);
|
|
66
|
+
if (!manifest) {
|
|
67
|
+
return {
|
|
68
|
+
store_exists: storeExists(input.cwd, input.preferredDirName),
|
|
69
|
+
freshness_badge: badge('missing_index'),
|
|
70
|
+
stats: null,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const base = badge(manifest.freshness.status, {
|
|
74
|
+
stale_file_count: manifest.freshness.stale_file_count,
|
|
75
|
+
partial_reason: manifest.freshness.partial_reason,
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
store_exists: true,
|
|
79
|
+
freshness_badge: this.withHeadDrift(base, manifest, input.cwd),
|
|
80
|
+
stats: {
|
|
81
|
+
files_indexed: manifest.stats.files_indexed,
|
|
82
|
+
nodes: manifest.stats.nodes,
|
|
83
|
+
edges: manifest.stats.edges,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Real refresh (spec §7): resolves project identity (input -> manifest ->
|
|
89
|
+
* cwd-derived default), then runs the Tree-sitter parse + index + materialize
|
|
90
|
+
* pipeline behind the project lock. A live competing lock fails fast with a
|
|
91
|
+
* clear status — refresh never blocks bclaw_work (rule 8).
|
|
92
|
+
*/
|
|
93
|
+
async refresh(input) {
|
|
94
|
+
const scope = input.scope ?? 'changed';
|
|
95
|
+
const manifest = readManifest(input.cwd, input.preferredDirName);
|
|
96
|
+
const projectRoot = input.projectRoot ?? manifest?.project_root ?? input.cwd ?? process.cwd();
|
|
97
|
+
const projectId = input.projectId ?? manifest?.project_id ?? `prj_${path.basename(path.resolve(projectRoot))}`;
|
|
98
|
+
const result = await runRefresh({
|
|
99
|
+
projectId,
|
|
100
|
+
projectRoot,
|
|
101
|
+
scope,
|
|
102
|
+
cwd: input.cwd,
|
|
103
|
+
preferredDirName: input.preferredDirName,
|
|
104
|
+
ownerAgent: input.ownerAgent ?? null,
|
|
105
|
+
ownerAgentId: input.ownerAgentId ?? null,
|
|
106
|
+
});
|
|
107
|
+
return {
|
|
108
|
+
ran: result.ran,
|
|
109
|
+
scope,
|
|
110
|
+
lock_acquired: result.lock_acquired,
|
|
111
|
+
freshness_badge: badge(result.freshness.status, {
|
|
112
|
+
stale_file_count: result.freshness.stale_file_count,
|
|
113
|
+
partial_reason: result.freshness.partial_reason,
|
|
114
|
+
files_parsed: result.files_parsed,
|
|
115
|
+
files_compacted: result.files_compacted,
|
|
116
|
+
duration_ms: result.duration_ms,
|
|
117
|
+
}),
|
|
118
|
+
...(result.lock_status ? { lock_status: result.lock_status } : {}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Agent-facing symbol search (spec §12.1). Ranks symbols-index matches and
|
|
123
|
+
* lazily validates each backing shard against the live file before serving it
|
|
124
|
+
* as confident (§6.1); the response badge reflects any detected drift.
|
|
125
|
+
*/
|
|
126
|
+
async find(input) {
|
|
127
|
+
const ctx = this.queryContext(input);
|
|
128
|
+
const out = runFind(input.query, input.limit, ctx);
|
|
129
|
+
const manifest = readManifest(input.cwd, input.preferredDirName);
|
|
130
|
+
const base = {
|
|
131
|
+
status: out.freshness_badge.status,
|
|
132
|
+
details: out.freshness_badge.details,
|
|
133
|
+
};
|
|
134
|
+
return {
|
|
135
|
+
query: out.query,
|
|
136
|
+
matches: out.matches,
|
|
137
|
+
freshness_badge: this.withHeadDrift(base, manifest, input.cwd),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Agent-facing reading list (spec §9, §11). Produces a ranked
|
|
142
|
+
* suggested_files_to_read (cap 12), attaches related brainclaw memory (cap 5),
|
|
143
|
+
* and carries a §6.1 lazy-validated freshness badge.
|
|
144
|
+
*/
|
|
145
|
+
async brief(input) {
|
|
146
|
+
const ctx = this.queryContext(input);
|
|
147
|
+
const out = runBrief(input.target, input.limit, ctx, this.memoryReader);
|
|
148
|
+
const manifest = readManifest(input.cwd, input.preferredDirName);
|
|
149
|
+
const base = {
|
|
150
|
+
status: out.freshness_badge.status,
|
|
151
|
+
details: out.freshness_badge.details,
|
|
152
|
+
};
|
|
153
|
+
return {
|
|
154
|
+
target: out.target,
|
|
155
|
+
suggested_files_to_read: out.suggested_files_to_read,
|
|
156
|
+
related_memory: out.related_memory,
|
|
157
|
+
freshness_badge: this.withHeadDrift(base, manifest, input.cwd),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Annotate a read badge with git-HEAD drift vs the commit the index was built
|
|
162
|
+
* against (`manifest.git.head`). trp_42688015 — a branch switch (whole-tree move)
|
|
163
|
+
* is otherwise unflagged because `status` reads only write-side freshness and the
|
|
164
|
+
* per-file lazy check is query-scoped + budgeted. No-op for non-git projects.
|
|
165
|
+
*/
|
|
166
|
+
withHeadDrift(base, manifest, cwd) {
|
|
167
|
+
if (!manifest)
|
|
168
|
+
return base;
|
|
169
|
+
const root = manifest.project_root || cwd || process.cwd();
|
|
170
|
+
return applyGitHeadDrift(base, manifest.git.head, this.gitHeadReader(root));
|
|
171
|
+
}
|
|
172
|
+
queryContext(input) {
|
|
173
|
+
return { cwd: input.cwd, preferredDirName: input.preferredDirName };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=backend.js.map
|