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 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.5.3
349
-
350
- - **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.
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
@@ -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 active = loadActiveProject(wsRoot);
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
- projects.push({ name: config.project_name, path: wsRoot, relative_path: '.', active: active?.path === wsRoot });
105
+ addProject({ name: config.project_name, path: wsRoot, relative_path: '.' });
84
106
  }
85
107
  catch {
86
- projects.push({ path: wsRoot, relative_path: '.', active: active?.path === wsRoot });
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
- projects.push({ name: child.project_name, path: childPath, relative_path: rel, active: active?.path === childPath });
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
@@ -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
- // 1. Look in sessions/ directory for a matching session
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 ttlMs = parseDurationToMs(loadConfigSafe(cwd)?.implicit_session_ttl ?? '4h');
82
- const now = Date.now();
91
+ const legacyPidlessCandidates = [];
83
92
  for (const file of files) {
84
93
  try {
85
- const migration = loadVersionedJsonFile('current_session', path.join(dir, file));
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
- const alive = (now - Date.parse(session.last_seen_at)) <= ttlMs;
95
- if (userMatch && alive) {
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
- if (process.env.CLAUDE_CODE_VERSION)
250
- return 'claude-code';
251
- return undefined;
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.0 on 2026-05-28T00:12:31.140Z
2
+ // Source: brainclaw v1.7.1 on 2026-06-02T10:24:49.702Z
3
3
  export const FACTS = {
4
- "version": "1.7.0",
5
- "generated_at": "2026-05-28T00:12:31.140Z",
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
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "1.7.0",
3
- "generated_at": "2026-05-28T00:12:31.140Z",
2
+ "version": "1.7.1",
3
+ "generated_at": "2026-06-02T10:24:49.702Z",
4
4
  "tools": {
5
5
  "count": 62,
6
6
  "published_count": 61,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainclaw",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "description": "Shared project memory for humans and coding agents.",
5
5
  "type": "module",
6
6
  "bin": {