brainclaw 1.26.0 → 1.26.2
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 -12
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/commands/session-start.js +54 -3
- package/dist/core/agent-files.js +21 -21
- package/dist/core/identity.js +256 -115
- package/dist/core/io.js +165 -0
- package/dist/core/protocol-tool-policy.js +44 -0
- package/dist/core/runtime.js +76 -11
- package/dist/core/store-resolution.js +5 -21
- package/dist/facts.js +6 -6
- package/dist/facts.json +5 -5
- package/docs/PROTOCOL.md +6 -4
- package/docs/cli.md +1 -1
- package/docs/concepts/loop-engine.md +60 -34
- package/docs/integrations/hermes.md +42 -3
- package/docs/integrations/mcp.md +17 -5
- package/docs/product/agent-first-model.md +33 -33
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ It sits alongside your coding agents and gives them a shared state layer they ca
|
|
|
46
46
|
| **Agent-ready context** | compact, prompt-sized context built from real workspace state instead of stale instructions |
|
|
47
47
|
| **Code Map** | a Tree-sitter symbol + import index (11 languages — JS/TS, Python, PHP, Java, Go, Rust, C#, Ruby, C, C++) so agents ask "where is X / what should I read first" before editing, with related decisions/traps attached — `bclaw_code_find` / `bclaw_code_brief`, see [code map](docs/code-map.md) |
|
|
48
48
|
| **Native agent files** | auto-writes `CLAUDE.md`, `AGENTS.md`, `GEMINI.md`, `.cursor/rules/`, `.windsurfrules`, and similar local guidance |
|
|
49
|
-
| **Multi-turn loops** | review and
|
|
49
|
+
| **Multi-turn loops** | review, ideation, implementation, research, and debug workflows with structured phases, iteration semantics, verification gates, and per-phase memory filters — see [loop engine](docs/concepts/loop-engine.md) |
|
|
50
50
|
| **Machine AI surface discovery** | detects local coding agents plus desktop AI work surfaces such as ChatGPT Desktop and Gemini CLI |
|
|
51
51
|
| **Queued surface tasks** | stores project-scoped requests for other local AI surfaces, such as visual generation, drafting, summaries, or research |
|
|
52
52
|
| **Local-first storage** | plain text + JSON, Git-friendly, no mandatory cloud, no telemetry by default |
|
|
@@ -263,14 +263,29 @@ bclaw_loop({ intent: "get", loop_id: "lop_abc" }); // inspect status any time
|
|
|
263
263
|
|
|
264
264
|
## The Loop Engine (Multi-Turn Workflows)
|
|
265
265
|
|
|
266
|
-
Brainclaw's Loop Engine
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
266
|
+
Brainclaw's Loop Engine formalizes repeated multi-turn work so agents can
|
|
267
|
+
resume, automate, and audit it rather than relying on manual ping-pong. It is
|
|
268
|
+
one engine with five shipped default workflows: **review, ideation,
|
|
269
|
+
implementation, research, and debug**.
|
|
270
|
+
|
|
271
|
+
| Workflow | Typical outcome | Normal entry point |
|
|
272
|
+
| --- | --- | --- |
|
|
273
|
+
| Review | accepted verdict or bounded fix cycle | `bclaw_coordinate(intent="review", open_loop=true)` |
|
|
274
|
+
| Ideation | memory-confronted plan draft or synthesis | `bclaw_coordinate(intent="ideate")` |
|
|
275
|
+
| Implementation | green verification and handoff | `bclaw_loop(intent="open", kind="implementation", allow_orphan=true)`, then `bind` |
|
|
276
|
+
| Research | evidence-backed synthesis | `bclaw_loop(intent="open", kind="research", allow_orphan=true)` |
|
|
277
|
+
| Debug | reproduced, verified fix and handoff | `bclaw_loop(intent="open", kind="debug", allow_orphan=true)` |
|
|
278
|
+
|
|
279
|
+
Every loop has structured phases, bounded iteration, explicit artifacts, and
|
|
280
|
+
per-phase memory filters. The shared controls are `open`, `turn`,
|
|
281
|
+
`complete_turn`, `advance`, `add_artifact`, `pause`, `resume`, and `close`;
|
|
282
|
+
implementation also adds `bind` and `verify`. `request_input` /
|
|
283
|
+
`provide_input` are cross-cutting clarification primitives for any workflow.
|
|
284
|
+
|
|
285
|
+
Review is a useful specialized path, not the definition of the engine. It has
|
|
286
|
+
asymmetric and symmetric modes and can auto-close on an approved verdict; the
|
|
287
|
+
other workflows use the same lifecycle to converge on a plan, synthesis,
|
|
288
|
+
handoff, or verified fix. See the [Loop Engine guide](docs/concepts/loop-engine.md).
|
|
274
289
|
|
|
275
290
|
## Enterprise Ready: Mono-repo & Micro-services
|
|
276
291
|
|
|
@@ -310,7 +325,7 @@ Recent releases have moved a lot of multi-agent parallel work from "risky" to "s
|
|
|
310
325
|
|
|
311
326
|
- **Per-claim auto-worktree** — each dispatched lane gets its own isolated git worktree; the coordinator integrates with an octopus merge.
|
|
312
327
|
- **Sequenced parallel execute** — `bclaw_dispatch(intent="execute")` fans out independent lanes across several agent instances and integrates the result.
|
|
313
|
-
- **
|
|
328
|
+
- **Loop Engine protocols** — review, ideation, implementation, research, and debug workflows share a persisted lifecycle. Review offers a symmetric auto-fix shortcut; implementation and debug bind verification to the work; ideation and research converge on durable syntheses.
|
|
314
329
|
- **Cross-platform spawn** — OS-aware prompt delivery (stdin pipe / inline arg) plus a brief-ack file handshake, so spawned workers can be detected and timed out reliably on Windows and Unix.
|
|
315
330
|
- **Worktree GC is scope-bounded** — symlinks and junctions are no longer followed during cleanup, so post-merge sweeps can't wipe `node_modules` or other neighboring directories.
|
|
316
331
|
- **MCP runtime self-heal** — when the runtime is corrupted, the server logs an actionable repair pointer; `brainclaw doctor --repair` rebuilds dist in one step.
|
|
@@ -327,8 +342,9 @@ Recommended use today:
|
|
|
327
342
|
|
|
328
343
|
1. for parallel work, dispatch a sequence with `bclaw_dispatch(intent="execute")` — each lane gets its own worktree
|
|
329
344
|
2. for sequential work in the same project, let one agent claim at a time and rely on handoffs
|
|
330
|
-
3.
|
|
331
|
-
4.
|
|
345
|
+
3. choose the loop by outcome: ideation for a plan, implementation or debug for a verified handoff, research for a synthesis, and review for a verdict
|
|
346
|
+
4. when reviewing or fixing across agents, prefer symmetric review loops over manual ping-pong
|
|
347
|
+
5. keep multi-machine workflows on a single source of truth until federation lands
|
|
332
348
|
|
|
333
349
|
---
|
|
334
350
|
|
|
Binary file
|
|
@@ -2,9 +2,9 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { execSync } from 'node:child_process';
|
|
5
|
-
import { isSessionSnapshotRecordFilename, memoryExists, resolveEntityDir, sessionSnapshotRecordPaths } from '../core/io.js';
|
|
5
|
+
import { assertSafeSessionId, isSessionSnapshotRecordFilename, memoryExists, resolveEntityDir, sessionSnapshotRecordPaths } from '../core/io.js';
|
|
6
6
|
import { loadVersionedJsonFile, saveVersionedJsonFile } from '../core/migration.js';
|
|
7
|
-
import { buildOperationalIdentity, loadAllSessions, saveCurrentSession } from '../core/identity.js';
|
|
7
|
+
import { buildOperationalIdentity, describeIgnoredSessionIdEnv, loadAllSessions, saveCurrentSession } from '../core/identity.js';
|
|
8
8
|
import { requireMinimumTrustLevel, resolveCurrentModel, resolveOrAutoRegisterAgentIdentity } from '../core/agent-registry.js';
|
|
9
9
|
import { buildContext, renderContextPromptTemplate } from '../core/context.js';
|
|
10
10
|
import { writeContextMarker } from '../core/freshness.js';
|
|
@@ -35,8 +35,39 @@ function sessionSnapshotWriteDir(cwd) {
|
|
|
35
35
|
return resolveEntityDir('sessions', cwd ?? process.cwd(), 'write');
|
|
36
36
|
}
|
|
37
37
|
function sessionSnapshotPath(sessionId, cwd) {
|
|
38
|
+
// pln#672 review P1 — THE SECOND WRITER. This builder was unguarded while
|
|
39
|
+
// sessionFilePathIn / sessionSnapshotRecordPaths were hardened, so an
|
|
40
|
+
// env-controlled traversal still escaped the store HERE, before the
|
|
41
|
+
// current_session write refused it (reproduced by the reviewer:
|
|
42
|
+
// `ESCAPED.snapshot.json` landed outside, and the later throw did not undo
|
|
43
|
+
// the escaped write). A guard on some builders is not a guard.
|
|
44
|
+
assertSafeSessionId(sessionId);
|
|
38
45
|
return path.join(sessionSnapshotWriteDir(cwd), `${sessionId}.snapshot.json`);
|
|
39
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* pln#672 review P2 — the snapshot write needs the same positive proof the
|
|
49
|
+
* current_session write already has. On a case-insensitive filesystem
|
|
50
|
+
* `CaseSnapshot` and `casesnapshot` name the SAME file, so a second session
|
|
51
|
+
* silently overwrote the first one's snapshot. Refuse to replace a record
|
|
52
|
+
* that names a different session; identical-id rewrites (heartbeat, restart)
|
|
53
|
+
* stay allowed.
|
|
54
|
+
*/
|
|
55
|
+
function assertSnapshotSlotFree(filepath, sessionId) {
|
|
56
|
+
if (!fs.existsSync(filepath))
|
|
57
|
+
return;
|
|
58
|
+
try {
|
|
59
|
+
const raw = JSON.parse(fs.readFileSync(filepath, 'utf-8'));
|
|
60
|
+
if (typeof raw.session_id === 'string' && raw.session_id !== sessionId) {
|
|
61
|
+
throw new Error(`Refusing to overwrite the session_snapshot of '${raw.session_id}' at '${filepath}' with '${sessionId}' — the two ids resolve to the same filename on this filesystem`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
// A deliberate refusal propagates; an unreadable record does not block a
|
|
66
|
+
// fresh write (it is not proof that another session owns the slot).
|
|
67
|
+
if (err instanceof Error && err.message.startsWith('Refusing to overwrite'))
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
40
71
|
/**
|
|
41
72
|
* pln#670 — lazy migration of pre-split snapshot records: rename `<id>.json` to
|
|
42
73
|
* `<id>.snapshot.json` in the CANONICAL sessions directory only. The legacy
|
|
@@ -145,6 +176,11 @@ export async function runSessionStart(options = {}) {
|
|
|
145
176
|
if (snapshot.stale_surfaces) {
|
|
146
177
|
console.warn(`⚠ ${snapshot.stale_surfaces.message}`);
|
|
147
178
|
}
|
|
179
|
+
// pln#672 — a dropped env session id must reach the human too: the whole
|
|
180
|
+
// point of the warning is that the session identity is NOT the one asked for.
|
|
181
|
+
if (snapshot.invalid_session_id_ignored) {
|
|
182
|
+
console.warn(`⚠ ${snapshot.invalid_session_id_ignored.message}`);
|
|
183
|
+
}
|
|
148
184
|
// Fifth instance of the computed-then-dropped class, caught by the new seam
|
|
149
185
|
// guard on its first run: built since the shared-checkout detection landed,
|
|
150
186
|
// read by nothing. Two agents editing one checkout is precisely what a human
|
|
@@ -202,7 +238,9 @@ export async function startSession(options = {}) {
|
|
|
202
238
|
const dir = sessionSnapshotWriteDir(options.cwd);
|
|
203
239
|
if (!fs.existsSync(dir))
|
|
204
240
|
fs.mkdirSync(dir, { recursive: true });
|
|
205
|
-
|
|
241
|
+
const snapshotPath = sessionSnapshotPath(snapshot.session_id, options.cwd);
|
|
242
|
+
assertSnapshotSlotFree(snapshotPath, snapshot.session_id);
|
|
243
|
+
saveVersionedJsonFile('session_snapshot', snapshotPath, SessionSnapshotSchema.parse(snapshot));
|
|
206
244
|
// Resolve git branch and worktree for session tracking
|
|
207
245
|
let currentBranch;
|
|
208
246
|
let currentWorktreePath;
|
|
@@ -371,6 +409,18 @@ export async function startSession(options = {}) {
|
|
|
371
409
|
staleSurfaces = staleSurfaceWarning(freshness, currentVersion);
|
|
372
410
|
}
|
|
373
411
|
catch { /* non-fatal */ }
|
|
412
|
+
// pln#672 — report a DROPPED env session id. Never echo the raw value: it is
|
|
413
|
+
// attacker-influenced and would land straight in logs; the variable name and
|
|
414
|
+
// its length are enough to diagnose.
|
|
415
|
+
let invalidSessionIdIgnored;
|
|
416
|
+
const droppedSessionEnv = describeIgnoredSessionIdEnv();
|
|
417
|
+
if (droppedSessionEnv) {
|
|
418
|
+
invalidSessionIdIgnored = {
|
|
419
|
+
code: 'invalid_session_id_ignored',
|
|
420
|
+
message: `${droppedSessionEnv.variable} carries a session id that cannot be a record filename (${droppedSessionEnv.length} chars) — it was ignored and this session runs under '${snapshot.session_id}'. Unset or fix the variable to resume the intended session.`,
|
|
421
|
+
data: { variable: droppedSessionEnv.variable, length: droppedSessionEnv.length, effective_session_id: snapshot.session_id },
|
|
422
|
+
};
|
|
423
|
+
}
|
|
374
424
|
// Materialize incoming federation signals from linked projects (Phase 0 — local)
|
|
375
425
|
if (maintenanceMode === 'full') {
|
|
376
426
|
try {
|
|
@@ -406,6 +456,7 @@ export async function startSession(options = {}) {
|
|
|
406
456
|
...(staleClaimsReleased ? { stale_claims_released: staleClaimsReleased } : {}),
|
|
407
457
|
...(memoryPressure ? { memory_pressure: memoryPressure } : {}),
|
|
408
458
|
...(staleSurfaces ? { stale_surfaces: toWarningDetail(staleSurfaces) } : {}),
|
|
459
|
+
...(invalidSessionIdIgnored ? { invalid_session_id_ignored: toWarningDetail(invalidSessionIdIgnored) } : {}),
|
|
409
460
|
...(autoRegistered ? { auto_registered: true } : {}),
|
|
410
461
|
};
|
|
411
462
|
}
|
package/dist/core/agent-files.js
CHANGED
|
@@ -3,7 +3,7 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
5
|
import yaml from 'yaml';
|
|
6
|
-
import { MCP_HEADLESS_AUTO_TOOL_NAMES, MCP_CANONICAL_GRAMMAR_TOOL_NAMES, REMOVED_IN_V1_TOOLS } from './protocol-tool-policy.js';
|
|
6
|
+
import { MCP_HEADLESS_AUTO_TOOL_NAMES, MCP_CANONICAL_GRAMMAR_TOOL_NAMES, MCP_HERMES_WORKFLOW_TOOL_NAMES, REMOVED_IN_V1_TOOLS } from './protocol-tool-policy.js';
|
|
7
7
|
import { renderToml, tomlArrayTableHasEntry } from './toml-writer.js';
|
|
8
8
|
import { PROTOCOL_SKILLS, renderProtocolSkill } from './protocol-skills.js';
|
|
9
9
|
import { getInstalledBrainclawVersion } from './brainclaw-version.js';
|
|
@@ -364,7 +364,6 @@ export const LOCAL_ONLY_AGENT_WORKSPACE_FILES = [
|
|
|
364
364
|
KILOCODE_MCP_RELATIVE_PATH,
|
|
365
365
|
KILOCODE_CONFIG_RELATIVE_PATH,
|
|
366
366
|
MISTRAL_VIBE_CONFIG_RELATIVE_PATH,
|
|
367
|
-
HERMES_CONFIG_RELATIVE_PATH,
|
|
368
367
|
CONTINUE_CONFIG_RELATIVE_PATH,
|
|
369
368
|
OPENCODE_CONFIG_RELATIVE_PATH,
|
|
370
369
|
WINDSURF_MCP_RELATIVE_PATH,
|
|
@@ -1553,25 +1552,22 @@ export function ensureMistralVibeMcpConfig(cwd) {
|
|
|
1553
1552
|
relativePath: MISTRAL_VIBE_CONFIG_RELATIVE_PATH,
|
|
1554
1553
|
};
|
|
1555
1554
|
}
|
|
1556
|
-
// Hermes' MCP `tools.include` array — narrow canonical-grammar surface. Derived
|
|
1557
|
-
// from MCP_CANONICAL_GRAMMAR_TOOL_NAMES (which is itself ALL_TOOLS-derived) so
|
|
1558
|
-
// new facade tools or canonical grammar verbs propagate without a manual edit
|
|
1559
|
-
// here (pln#546 step 2). REMOVED_IN_V1_TOOLS are stripped so deprecated names
|
|
1560
|
-
// don't reappear in user-facing configs.
|
|
1561
|
-
//
|
|
1562
|
-
// LAZY (pln#564 coordinator fix): computed on first call, NOT at module init.
|
|
1563
|
-
// agent-files.ts ↔ commands/mcp.ts form an import cycle; reading the imported
|
|
1564
|
-
// MCP_CANONICAL_GRAMMAR_TOOL_NAMES at module-eval time threw a TDZ
|
|
1565
|
-
// ("Cannot access 'MCP_CANONICAL_GRAMMAR_TOOL_NAMES' before initialization")
|
|
1566
|
-
// when agent-files loaded mid-mcp-init — which broke the MCP server. tsc does
|
|
1567
|
-
// not catch this (runtime-only). Deferring the read to call time fixes it.
|
|
1568
|
-
let hermesBrainclawMcpToolsCache;
|
|
1569
1555
|
function getHermesBrainclawMcpTools() {
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
return
|
|
1556
|
+
return MCP_HERMES_WORKFLOW_TOOL_NAMES
|
|
1557
|
+
.filter((name) => !REMOVED_IN_V1_TOOLS.has(name));
|
|
1558
|
+
}
|
|
1559
|
+
function hasExactMcpToolList(value, expected) {
|
|
1560
|
+
return Array.isArray(value)
|
|
1561
|
+
&& value.length === expected.length
|
|
1562
|
+
&& value.every((tool, index) => tool === expected[index]);
|
|
1563
|
+
}
|
|
1564
|
+
function isLegacyHermesBrainclawMcpTools(value) {
|
|
1565
|
+
// The original Hermes writer emitted precisely the canonical seven-tool
|
|
1566
|
+
// list. Upgrade that known managed value, but preserve every other list as
|
|
1567
|
+
// an explicit user customization.
|
|
1568
|
+
const legacyTools = MCP_CANONICAL_GRAMMAR_TOOL_NAMES
|
|
1569
|
+
.filter((name) => !REMOVED_IN_V1_TOOLS.has(name));
|
|
1570
|
+
return hasExactMcpToolList(value, legacyTools);
|
|
1575
1571
|
}
|
|
1576
1572
|
export function ensureHermesMcpConfig(homeDir, workspacePath) {
|
|
1577
1573
|
if (!homeDir)
|
|
@@ -1617,6 +1613,8 @@ export function ensureHermesMcpConfig(homeDir, workspacePath) {
|
|
|
1617
1613
|
}
|
|
1618
1614
|
}
|
|
1619
1615
|
const mcpCmd = getBrainclawMcpCommand();
|
|
1616
|
+
const existingInclude = currentTools.include;
|
|
1617
|
+
const managedInclude = getHermesBrainclawMcpTools();
|
|
1620
1618
|
const desiredEntry = {
|
|
1621
1619
|
...current,
|
|
1622
1620
|
command: typeof current.command === 'string' ? current.command : mcpCmd.command,
|
|
@@ -1627,7 +1625,9 @@ export function ensureHermesMcpConfig(homeDir, workspacePath) {
|
|
|
1627
1625
|
},
|
|
1628
1626
|
tools: {
|
|
1629
1627
|
...currentTools,
|
|
1630
|
-
include: Array.isArray(
|
|
1628
|
+
include: Array.isArray(existingInclude) && !isLegacyHermesBrainclawMcpTools(existingInclude)
|
|
1629
|
+
? existingInclude
|
|
1630
|
+
: managedInclude,
|
|
1631
1631
|
prompts: typeof currentTools.prompts === 'boolean' ? currentTools.prompts : false,
|
|
1632
1632
|
resources: typeof currentTools.resources === 'boolean' ? currentTools.resources : false,
|
|
1633
1633
|
},
|