brainclaw 1.7.0 → 1.7.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 +28 -5
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/commands/switch.js +40 -8
- package/dist/core/identity.js +40 -13
- package/dist/facts.js +3 -3
- package/dist/facts.json +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -343,11 +343,34 @@ npm run test:coverage # with coverage report
|
|
|
343
343
|
|
|
344
344
|
## Changelog
|
|
345
345
|
|
|
346
|
-
For older releases (v0.x and the early v1.0 launch series), `git log` on `master` is the source of truth — every release commit follows the `chore(release): bump version to <semver>` convention, and the matching feature/fix commits reference their plan id (e.g. `feat(mcp): self-heal ... (pln#478)`).
|
|
347
|
-
|
|
348
|
-
### v1.
|
|
349
|
-
|
|
350
|
-
- **
|
|
346
|
+
For older releases (v0.x and the early v1.0 launch series), `git log` on `master` is the source of truth — every release commit follows the `chore(release): bump version to <semver>` convention, and the matching feature/fix commits reference their plan id (e.g. `feat(mcp): self-heal ... (pln#478)`).
|
|
347
|
+
|
|
348
|
+
### v1.7.1
|
|
349
|
+
|
|
350
|
+
- **MCP project context isolation fix** — `bclaw_switch` now keeps MCP switches
|
|
351
|
+
session-scoped even when the agent session has to be resolved or created on
|
|
352
|
+
the fly. Session lookup honors explicit session IDs, avoids adopting another
|
|
353
|
+
live process's session, detects Codex via native `CODEX_*` runtime variables,
|
|
354
|
+
and `bclaw_switch(list=true)` reports the session active project with
|
|
355
|
+
`active_source`.
|
|
356
|
+
|
|
357
|
+
### v1.7.0
|
|
358
|
+
|
|
359
|
+
- **Dispatch reliability + scope-aware dirty guard** — evidence-first
|
|
360
|
+
`agent_run` reconciliation avoids false terminal states, `bclaw_coordinate`
|
|
361
|
+
accepts pinned refs and a scope-aware `allow_dirty` guard, and the Hermes
|
|
362
|
+
agent integration joins the supported surfaces.
|
|
363
|
+
|
|
364
|
+
### v1.6.0
|
|
365
|
+
|
|
366
|
+
- **Bootstrap loop + cross-project agent workflow** — the bootstrap ideation
|
|
367
|
+
preset can materialize `PROJECT.md`, `bclaw_init_project` initializes and links
|
|
368
|
+
arbitrary project paths, and `project=` routing reaches `bclaw_work` /
|
|
369
|
+
`bclaw_loop` for linked-project operations.
|
|
370
|
+
|
|
371
|
+
### v1.5.3
|
|
372
|
+
|
|
373
|
+
- **Cross-project canonical grammar + CLI parity** (pln#359, all phases) — the canonical grammar (`bclaw_find / get / create / update / remove / transition`), `bclaw_context`, and `bclaw_coordinate` now accept an optional `project: <name>` argument that routes the operation to a linked project. Two link kinds are recognised: `cross_project_links` (sibling/peer projects in `config.yaml`, `brainclaw link list`) and workspace store-chain children. Arbitrary directory paths are rejected — adoption requires an explicit link, which gives the user a single point of control over what an agent can reach. Identity is sourced from the caller's home registry; entity writes + audit log entries land in the target. Unknown project names throw `validation_error` with a hint listing the configured links — no silent fallback. Cross-project `bclaw_coordinate` is **inbox-only**: claim/assignment/message all land in the target, the target agent picks the brief up async via its own `bclaw_work`, and auto-spawn from the source process is force-disabled because the spawn cwd / worktree are tied to the target's git repo (a warning surfaces in `FacadeResponse.warnings`). The CLI exposes the same as a global `--project <name>` flag, mutually exclusive with `--cwd`. Refs: helper `resolveProjectCwd` in `src/core/cross-project.ts`, MCP write/read handler dispatch in `src/commands/mcp.ts` and `src/commands/mcp-read-handlers.ts`, `--project` plumbing in `src/cli.ts` preAction, surface advertisement in `src/core/instruction-templates.ts`, plus tests in `tests/unit/cross-project.test.ts` (10 unit cases on the helper), `tests/unit/bclaw-coordinate.test.ts` (4 cross-project routing cases), and `tests/cli-cross-project.test.ts` (5 e2e cases). Closes the `--cwd` workaround pattern that had been the day-to-day shape of multi-project sessions.
|
|
351
374
|
- **Site facts contract** (umbrella `pln_7fdfd70d` sprint 0) — new `scripts/emit-site-facts.mjs` emits `dist/facts.{js,json}` from `MCP_TOOL_NAMES` + `ENTITY_NAMES` so the brainclaw-site (and any consumer) can pull live tool/entity counts at build time without forking the values into a hand-maintained config. The package `files` list ships `dist/facts.json`; build:cli runs the emitter as part of the chain.
|
|
352
375
|
|
|
353
376
|
### v1.5.2
|
|
Binary file
|
package/dist/commands/switch.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { loadActiveProject, saveActiveProject, clearActiveProject } from '../core/active-project.js';
|
|
3
|
-
import { loadCurrentSession, saveCurrentSession } from '../core/identity.js';
|
|
3
|
+
import { buildOperationalIdentity, loadCurrentSession, saveCurrentSession } from '../core/identity.js';
|
|
4
4
|
import { memoryExists } from '../core/io.js';
|
|
5
5
|
import { resolveProjectRef } from '../core/store-resolution.js';
|
|
6
|
-
import { resolveProjectCwd } from '../core/cross-project.js';
|
|
6
|
+
import { resolveCrossProjectLinks, resolveProjectCwd } from '../core/cross-project.js';
|
|
7
7
|
import { scanNestedBrainclawProjects } from '../core/workspace-projects.js';
|
|
8
8
|
import { loadConfig } from '../core/config.js';
|
|
9
9
|
/**
|
|
@@ -43,8 +43,12 @@ export function switchProject(projectRef, options = {}) {
|
|
|
43
43
|
}
|
|
44
44
|
catch { /* name is optional */ }
|
|
45
45
|
const now = new Date().toISOString();
|
|
46
|
-
const session = loadCurrentSession(cwd);
|
|
47
46
|
const sessionOnly = options.sessionOnly ?? true;
|
|
47
|
+
let session = loadCurrentSession(cwd);
|
|
48
|
+
if (!session && sessionOnly) {
|
|
49
|
+
buildOperationalIdentity(undefined, cwd, { persistImplicitSession: true });
|
|
50
|
+
session = loadCurrentSession(cwd);
|
|
51
|
+
}
|
|
48
52
|
if (session && sessionOnly) {
|
|
49
53
|
saveCurrentSession({
|
|
50
54
|
...session,
|
|
@@ -52,6 +56,9 @@ export function switchProject(projectRef, options = {}) {
|
|
|
52
56
|
}, cwd);
|
|
53
57
|
return { switched: true, path: resolved, name: projectName, scope: 'session', workspace_root: wsRoot };
|
|
54
58
|
}
|
|
59
|
+
if (sessionOnly) {
|
|
60
|
+
throw new Error('Cannot switch project without an active agent session. Start with bclaw_work or bclaw_session_start first.');
|
|
61
|
+
}
|
|
55
62
|
if (session) {
|
|
56
63
|
// Also write to session even when not sessionOnly
|
|
57
64
|
saveCurrentSession({
|
|
@@ -75,15 +82,30 @@ export function listAvailableProjects(cwd) {
|
|
|
75
82
|
if (!wsRoot) {
|
|
76
83
|
throw new Error('No brainclaw workspace found.');
|
|
77
84
|
}
|
|
78
|
-
const
|
|
85
|
+
const sessionActive = loadCurrentSession(cwd)?.active_project;
|
|
86
|
+
const globalActive = loadActiveProject(wsRoot);
|
|
87
|
+
const active = sessionActive ?? globalActive;
|
|
88
|
+
const activeSource = sessionActive ? 'session' : globalActive ? 'global' : 'none';
|
|
79
89
|
const projects = [];
|
|
90
|
+
const seen = new Set();
|
|
91
|
+
const addProject = (project) => {
|
|
92
|
+
const projectPath = path.resolve(project.path);
|
|
93
|
+
if (seen.has(projectPath))
|
|
94
|
+
return;
|
|
95
|
+
seen.add(projectPath);
|
|
96
|
+
projects.push({
|
|
97
|
+
...project,
|
|
98
|
+
path: projectPath,
|
|
99
|
+
active: active?.path ? path.resolve(active.path) === projectPath : false,
|
|
100
|
+
});
|
|
101
|
+
};
|
|
80
102
|
if (memoryExists(wsRoot)) {
|
|
81
103
|
try {
|
|
82
104
|
const config = loadConfig(wsRoot);
|
|
83
|
-
|
|
105
|
+
addProject({ name: config.project_name, path: wsRoot, relative_path: '.' });
|
|
84
106
|
}
|
|
85
107
|
catch {
|
|
86
|
-
|
|
108
|
+
addProject({ path: wsRoot, relative_path: '.' });
|
|
87
109
|
}
|
|
88
110
|
}
|
|
89
111
|
const children = scanNestedBrainclawProjects(wsRoot, 7);
|
|
@@ -92,9 +114,19 @@ export function listAvailableProjects(cwd) {
|
|
|
92
114
|
if (childPath === wsRoot)
|
|
93
115
|
continue;
|
|
94
116
|
const rel = path.relative(wsRoot, childPath) || '.';
|
|
95
|
-
|
|
117
|
+
addProject({ name: child.project_name, path: childPath, relative_path: rel });
|
|
118
|
+
}
|
|
119
|
+
for (const link of resolveCrossProjectLinks(wsRoot)) {
|
|
120
|
+
if (!link.available)
|
|
121
|
+
continue;
|
|
122
|
+
const linkPath = path.resolve(link.absolutePath);
|
|
123
|
+
addProject({
|
|
124
|
+
name: link.projectName,
|
|
125
|
+
path: linkPath,
|
|
126
|
+
relative_path: path.relative(wsRoot, linkPath) || '.',
|
|
127
|
+
});
|
|
96
128
|
}
|
|
97
|
-
return { workspace_root: wsRoot, projects };
|
|
129
|
+
return { workspace_root: wsRoot, active_source: activeSource, projects };
|
|
98
130
|
}
|
|
99
131
|
export function runSwitch(projectRef, options = {}) {
|
|
100
132
|
// Use real cwd, not effective cwd — switch must see the full workspace
|
package/dist/core/identity.js
CHANGED
|
@@ -2,6 +2,7 @@ import crypto from 'node:crypto';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { detectAiAgent } from './ai-agent-detection.js';
|
|
5
6
|
import { requireRegisteredAgentIdentity } from './agent-registry.js';
|
|
6
7
|
import { loadConfig } from './config.js';
|
|
7
8
|
import { resolveCurrentHostId } from './host.js';
|
|
@@ -75,31 +76,42 @@ export function loadCurrentSession(cwd) {
|
|
|
75
76
|
const dir = sessionsDir(cwd);
|
|
76
77
|
const currentUser = resolveCurrentUser();
|
|
77
78
|
const currentAgent = resolveCurrentAgentName();
|
|
78
|
-
|
|
79
|
+
const explicitSessionId = resolveExplicitSessionId();
|
|
80
|
+
const ttlMs = parseDurationToMs(loadConfigSafe(cwd)?.implicit_session_ttl ?? '4h');
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
if (explicitSessionId) {
|
|
83
|
+
const explicit = loadSessionById(explicitSessionId, cwd);
|
|
84
|
+
return explicit && isSessionAlive(explicit, ttlMs, now) ? explicit : undefined;
|
|
85
|
+
}
|
|
86
|
+
// 1. Look in sessions/ directory for the session owned by this process.
|
|
87
|
+
// Multiple parallel agents can have the same agent name/user in one repo;
|
|
88
|
+
// a live different PID is a different agent instance, not our session.
|
|
79
89
|
if (fs.existsSync(dir) && currentAgent) {
|
|
80
90
|
const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
81
|
-
const
|
|
82
|
-
const now = Date.now();
|
|
91
|
+
const legacyPidlessCandidates = [];
|
|
83
92
|
for (const file of files) {
|
|
84
93
|
try {
|
|
85
|
-
const
|
|
86
|
-
const session = {
|
|
87
|
-
...CurrentSessionStateSchema.parse(migration.document),
|
|
88
|
-
schema_version: migration.metadata.currentVersion,
|
|
89
|
-
};
|
|
94
|
+
const session = loadSessionFile(path.join(dir, file));
|
|
90
95
|
// Strict match: agent name must match, user must match (when both are known)
|
|
91
96
|
if (session.agent !== currentAgent)
|
|
92
97
|
continue;
|
|
93
98
|
const userMatch = !session.user || !currentUser || session.user === currentUser;
|
|
94
|
-
|
|
95
|
-
|
|
99
|
+
if (!userMatch || !isSessionAlive(session, ttlMs, now))
|
|
100
|
+
continue;
|
|
101
|
+
if (session.pid === process.pid) {
|
|
96
102
|
return session;
|
|
97
103
|
}
|
|
104
|
+
if (session.pid === undefined) {
|
|
105
|
+
legacyPidlessCandidates.push(session);
|
|
106
|
+
}
|
|
98
107
|
}
|
|
99
108
|
catch {
|
|
100
109
|
// skip invalid session files
|
|
101
110
|
}
|
|
102
111
|
}
|
|
112
|
+
if (legacyPidlessCandidates.length === 1) {
|
|
113
|
+
return legacyPidlessCandidates[0];
|
|
114
|
+
}
|
|
103
115
|
}
|
|
104
116
|
// 2. Legacy fallback: .current-session
|
|
105
117
|
const legacyPath = path.join(memoryDir(cwd), LEGACY_SESSION_FILE);
|
|
@@ -246,9 +258,24 @@ function resolveCurrentUser() {
|
|
|
246
258
|
function resolveCurrentAgentName() {
|
|
247
259
|
if (process.env.BRAINCLAW_AGENT_NAME)
|
|
248
260
|
return process.env.BRAINCLAW_AGENT_NAME;
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
261
|
+
return detectAiAgent()?.name;
|
|
262
|
+
}
|
|
263
|
+
function resolveExplicitSessionId(env = process.env) {
|
|
264
|
+
return env.BRAINCLAW_SESSION_ID?.trim()
|
|
265
|
+
|| env.OPENCLAW_SESSION_ID?.trim()
|
|
266
|
+
|| env.CLAUDE_SESSION_ID?.trim()
|
|
267
|
+
|| env.COPILOT_SESSION_ID?.trim()
|
|
268
|
+
|| undefined;
|
|
269
|
+
}
|
|
270
|
+
function loadSessionFile(filepath) {
|
|
271
|
+
const migration = loadVersionedJsonFile('current_session', filepath);
|
|
272
|
+
return {
|
|
273
|
+
...CurrentSessionStateSchema.parse(migration.document),
|
|
274
|
+
schema_version: migration.metadata.currentVersion,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function isSessionAlive(session, ttlMs, now) {
|
|
278
|
+
return now - Date.parse(session.last_seen_at) <= ttlMs;
|
|
252
279
|
}
|
|
253
280
|
function loadConfigSafe(cwd) {
|
|
254
281
|
try {
|
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.7.
|
|
2
|
+
// Source: brainclaw v1.7.1 on 2026-06-02T10:24:49.702Z
|
|
3
3
|
export const FACTS = {
|
|
4
|
-
"version": "1.7.
|
|
5
|
-
"generated_at": "2026-
|
|
4
|
+
"version": "1.7.1",
|
|
5
|
+
"generated_at": "2026-06-02T10:24:49.702Z",
|
|
6
6
|
"tools": {
|
|
7
7
|
"count": 62,
|
|
8
8
|
"published_count": 61,
|
package/dist/facts.json
CHANGED