brainclaw 0.23.0 → 0.24.0
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 +20 -8
- package/dist/commands/accept.js +5 -2
- package/dist/commands/claim.js +5 -4
- package/dist/commands/instruction.js +5 -4
- package/dist/commands/mcp.js +27 -0
- package/dist/commands/prune.js +5 -4
- package/dist/commands/reflect.js +2 -0
- package/dist/commands/release-claim.js +4 -4
- package/dist/commands/release-claims.js +5 -4
- package/dist/commands/switch.js +2 -2
- package/dist/core/ai-surface-tasks.js +3 -2
- package/dist/core/audit.js +3 -2
- package/dist/core/bootstrap.js +7 -6
- package/dist/core/candidates.js +4 -3
- package/dist/core/claims.js +3 -2
- package/dist/core/instructions.js +3 -2
- package/dist/core/io.js +1 -0
- package/dist/core/lock.js +2 -2
- package/dist/core/markdown.js +18 -0
- package/dist/core/mutation-pipeline.js +39 -0
- package/dist/core/runtime.js +4 -3
- package/dist/core/state.js +5 -4
- package/docs/architecture/project-refs.md +19 -0
- package/docs/cli.md +47 -0
- package/docs/concepts/memory.md +4 -3
- package/docs/concepts/workspace-bootstrapping.md +41 -0
- package/docs/integrations/agents.md +35 -0
- package/docs/integrations/claude-code.md +1 -1
- package/docs/integrations/codex.md +1 -1
- package/docs/integrations/copilot.md +1 -1
- package/docs/integrations/cursor.md +1 -1
- package/docs/integrations/mcp.md +15 -5
- package/docs/integrations/overview.md +2 -2
- package/docs/quickstart.md +3 -1
- package/docs/storage.md +51 -24
- 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';
|
|
@@ -94,15 +95,26 @@ program
|
|
|
94
95
|
.hook('preAction', (_thisCommand, actionCommand) => {
|
|
95
96
|
const root = actionCommand.optsWithGlobals();
|
|
96
97
|
initLogLevel({ verbose: root.verbose, debug: root.debug });
|
|
97
|
-
//
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
+
}
|
|
102
114
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
115
|
+
else if (root.cwd) {
|
|
116
|
+
// For init/setup, still respect explicit --cwd but nothing else
|
|
117
|
+
process.chdir(path.resolve(root.cwd));
|
|
106
118
|
}
|
|
107
119
|
});
|
|
108
120
|
// --- init ---
|
package/dist/commands/accept.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { memoryExists
|
|
1
|
+
import { memoryExists } from '../core/io.js';
|
|
2
|
+
import { mutate } from '../core/mutation-pipeline.js';
|
|
3
|
+
import { rebuildProjectMd } from '../core/markdown.js';
|
|
2
4
|
import { loadCandidate, archiveCandidate, resolveIdOrAlias } from '../core/candidates.js';
|
|
3
5
|
import { loadState, persistState } from '../core/state.js';
|
|
4
6
|
import { generateIdWithLabel, nowISO } from '../core/ids.js';
|
|
@@ -36,7 +38,7 @@ export function acceptCandidate(id, by, cwd, byId) {
|
|
|
36
38
|
requireMinimumTrustLevel(actorIdentity, 'trusted');
|
|
37
39
|
const actor = actorIdentity.agent_name;
|
|
38
40
|
let promotedItemId = '';
|
|
39
|
-
|
|
41
|
+
mutate({ cwd }, () => {
|
|
40
42
|
const state = loadState(cwd);
|
|
41
43
|
switch (candidate.type) {
|
|
42
44
|
case 'constraint': {
|
|
@@ -136,6 +138,7 @@ export function acceptCandidate(id, by, cwd, byId) {
|
|
|
136
138
|
after: { type: candidate.type, text: candidate.text },
|
|
137
139
|
reason: 'trusted-agent',
|
|
138
140
|
}, cwd);
|
|
141
|
+
rebuildProjectMd(loadState(cwd), cwd);
|
|
139
142
|
});
|
|
140
143
|
return {
|
|
141
144
|
candidate_id: resolvedId,
|
package/dist/commands/claim.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { buildOperationalIdentity } from '../core/identity.js';
|
|
2
|
-
import { memoryExists
|
|
2
|
+
import { memoryExists } from '../core/io.js';
|
|
3
|
+
import { mutate } from '../core/mutation-pipeline.js';
|
|
3
4
|
import { saveClaim, generateClaimId, listClaims } from '../core/claims.js';
|
|
4
|
-
import {
|
|
5
|
+
import { rebuildProjectMd } from '../core/markdown.js';
|
|
5
6
|
import { loadState, saveState } from '../core/state.js';
|
|
6
7
|
import { nowISO } from '../core/ids.js';
|
|
7
8
|
import { requireMinimumTrustLevel, requireRegisteredAgentIdentity } from '../core/agent-registry.js';
|
|
@@ -72,7 +73,7 @@ export function runClaim(description, options) {
|
|
|
72
73
|
status: 'active',
|
|
73
74
|
expires_at: options.ttl ? parseTtl(options.ttl) : undefined,
|
|
74
75
|
};
|
|
75
|
-
|
|
76
|
+
mutate({ cwd: options.cwd }, () => {
|
|
76
77
|
if (plan) {
|
|
77
78
|
if (!plan.assignee) {
|
|
78
79
|
plan.assignee = actor.agent;
|
|
@@ -84,7 +85,7 @@ export function runClaim(description, options) {
|
|
|
84
85
|
saveState(state, options.cwd);
|
|
85
86
|
}
|
|
86
87
|
saveClaim(claim, options.cwd);
|
|
87
|
-
|
|
88
|
+
rebuildProjectMd(plan ? state : loadState(options.cwd), options.cwd);
|
|
88
89
|
});
|
|
89
90
|
const planInfo = claim.plan_id ? ` [plan ${claim.plan_id}]` : '';
|
|
90
91
|
const ttlInfo = claim.expires_at ? ` (expires ${claim.expires_at.slice(0, 16).replace('T', ' ')})` : '';
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { resolveAgentScope, resolveCurrentAgentName } from '../core/agent-registry.js';
|
|
2
2
|
import { loadConfig } from '../core/config.js';
|
|
3
3
|
import { createInstruction } from '../core/instructions.js';
|
|
4
|
-
import { memoryExists
|
|
5
|
-
import {
|
|
4
|
+
import { memoryExists } from '../core/io.js';
|
|
5
|
+
import { mutate } from '../core/mutation-pipeline.js';
|
|
6
|
+
import { rebuildProjectMd } from '../core/markdown.js';
|
|
6
7
|
import { loadState } from '../core/state.js';
|
|
7
8
|
import { scanText } from '../core/security.js';
|
|
8
9
|
import { validateCliInput } from '../core/input-validation.js';
|
|
@@ -26,7 +27,7 @@ export function runInstruction(text, options = {}) {
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
let entry;
|
|
29
|
-
|
|
30
|
+
mutate({ cwd }, () => {
|
|
30
31
|
entry = createInstruction(text, {
|
|
31
32
|
layer,
|
|
32
33
|
scope,
|
|
@@ -34,7 +35,7 @@ export function runInstruction(text, options = {}) {
|
|
|
34
35
|
author: options.author ?? resolveCurrentAgentName(cwd),
|
|
35
36
|
supersedes: options.supersedes,
|
|
36
37
|
}, cwd);
|
|
37
|
-
|
|
38
|
+
rebuildProjectMd(loadState(cwd), cwd);
|
|
38
39
|
});
|
|
39
40
|
if (!entry) {
|
|
40
41
|
console.error('Error: failed to persist instruction.');
|
package/dist/commands/mcp.js
CHANGED
|
@@ -667,6 +667,11 @@ export class McpTaskRunner {
|
|
|
667
667
|
onInternalError;
|
|
668
668
|
active;
|
|
669
669
|
queue = [];
|
|
670
|
+
_totalExecuted = 0;
|
|
671
|
+
_totalCancelled = 0;
|
|
672
|
+
_peakQueueDepth = 0;
|
|
673
|
+
_lastDurationMs = 0;
|
|
674
|
+
_lastWaitMs = 0;
|
|
670
675
|
constructor(options) {
|
|
671
676
|
this.executeTool = options.executeTool;
|
|
672
677
|
this.onResult = options.onResult;
|
|
@@ -678,19 +683,35 @@ export class McpTaskRunner {
|
|
|
678
683
|
get queuedRequestIds() {
|
|
679
684
|
return this.queue.map((task) => task.requestId);
|
|
680
685
|
}
|
|
686
|
+
/** Current single-writer queue metrics. */
|
|
687
|
+
get metrics() {
|
|
688
|
+
return {
|
|
689
|
+
totalExecuted: this._totalExecuted,
|
|
690
|
+
totalCancelled: this._totalCancelled,
|
|
691
|
+
queueDepth: this.queue.length,
|
|
692
|
+
peakQueueDepth: this._peakQueueDepth,
|
|
693
|
+
lastDurationMs: this._lastDurationMs,
|
|
694
|
+
lastWaitMs: this._lastWaitMs,
|
|
695
|
+
};
|
|
696
|
+
}
|
|
681
697
|
enqueue(requestId, payload) {
|
|
682
698
|
this.queue.push({
|
|
683
699
|
requestId,
|
|
684
700
|
payload,
|
|
685
701
|
controller: new AbortController(),
|
|
686
702
|
cancelled: false,
|
|
703
|
+
enqueuedAt: performance.now(),
|
|
687
704
|
});
|
|
705
|
+
if (this.queue.length > this._peakQueueDepth) {
|
|
706
|
+
this._peakQueueDepth = this.queue.length;
|
|
707
|
+
}
|
|
688
708
|
this.drain();
|
|
689
709
|
}
|
|
690
710
|
cancel(requestId) {
|
|
691
711
|
if (this.active && this.active.requestId === requestId) {
|
|
692
712
|
this.active.cancelled = true;
|
|
693
713
|
this.active.controller.abort();
|
|
714
|
+
this._totalCancelled++;
|
|
694
715
|
return 'active';
|
|
695
716
|
}
|
|
696
717
|
const index = this.queue.findIndex((task) => task.requestId === requestId);
|
|
@@ -698,6 +719,7 @@ export class McpTaskRunner {
|
|
|
698
719
|
const [task] = this.queue.splice(index, 1);
|
|
699
720
|
task.cancelled = true;
|
|
700
721
|
task.controller.abort();
|
|
722
|
+
this._totalCancelled++;
|
|
701
723
|
return 'queued';
|
|
702
724
|
}
|
|
703
725
|
return 'missing';
|
|
@@ -722,6 +744,7 @@ export class McpTaskRunner {
|
|
|
722
744
|
return;
|
|
723
745
|
}
|
|
724
746
|
if (next.cancelled) {
|
|
747
|
+
this._totalCancelled++;
|
|
725
748
|
this.drain();
|
|
726
749
|
return;
|
|
727
750
|
}
|
|
@@ -729,6 +752,8 @@ export class McpTaskRunner {
|
|
|
729
752
|
void this.runTask(next);
|
|
730
753
|
}
|
|
731
754
|
async runTask(task) {
|
|
755
|
+
const startedAt = performance.now();
|
|
756
|
+
this._lastWaitMs = startedAt - task.enqueuedAt;
|
|
732
757
|
try {
|
|
733
758
|
const outcome = await this.executeTool(task.payload, task.controller.signal);
|
|
734
759
|
if (!task.cancelled) {
|
|
@@ -741,6 +766,8 @@ export class McpTaskRunner {
|
|
|
741
766
|
}
|
|
742
767
|
}
|
|
743
768
|
finally {
|
|
769
|
+
this._lastDurationMs = performance.now() - startedAt;
|
|
770
|
+
this._totalExecuted++;
|
|
744
771
|
if (this.active === task) {
|
|
745
772
|
this.active = undefined;
|
|
746
773
|
}
|
package/dist/commands/prune.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { loadState, saveState } from '../core/state.js';
|
|
2
|
-
import { memoryExists
|
|
3
|
-
import {
|
|
2
|
+
import { memoryExists } from '../core/io.js';
|
|
3
|
+
import { mutate } from '../core/mutation-pipeline.js';
|
|
4
|
+
import { rebuildProjectMd } from '../core/markdown.js';
|
|
4
5
|
import { deleteRuntimeNote, listRuntimeNotes } from '../core/runtime.js';
|
|
5
6
|
import { expireStaleActiveClaims } from '../core/claims.js';
|
|
6
7
|
export function runPrune(options = {}) {
|
|
@@ -13,7 +14,7 @@ export function runPrune(options = {}) {
|
|
|
13
14
|
let prunedCount = 0;
|
|
14
15
|
let expiredClaimsCount = 0;
|
|
15
16
|
let expiredNotesCount = 0;
|
|
16
|
-
|
|
17
|
+
mutate({ cwd }, () => {
|
|
17
18
|
const state = loadState(cwd);
|
|
18
19
|
const originalLength = state.active_constraints.length;
|
|
19
20
|
for (const c of state.active_constraints) {
|
|
@@ -38,7 +39,7 @@ export function runPrune(options = {}) {
|
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
|
-
|
|
42
|
+
rebuildProjectMd(loadState(cwd), cwd);
|
|
42
43
|
});
|
|
43
44
|
if (options.expired) {
|
|
44
45
|
console.log(`✔ Pruned ${prunedCount} expired constraints, ${expiredNotesCount} expired runtime notes, ${expiredClaimsCount} expired claims.`);
|
package/dist/commands/reflect.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { memoryExists } from '../core/io.js';
|
|
3
|
+
import { rebuildProjectMd } from '../core/markdown.js';
|
|
3
4
|
import { loadConfig } from '../core/config.js';
|
|
4
5
|
import { buildOperationalIdentity } from '../core/identity.js';
|
|
5
6
|
import { loadState, persistState } from '../core/state.js';
|
|
@@ -265,6 +266,7 @@ function promoteCandidateToState(candidate, cwd) {
|
|
|
265
266
|
}
|
|
266
267
|
}
|
|
267
268
|
persistState(state, cwd);
|
|
269
|
+
rebuildProjectMd(loadState(cwd), cwd);
|
|
268
270
|
return promotedItemId;
|
|
269
271
|
}
|
|
270
272
|
export function mapEventTypeToCandidateType(eventType) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { memoryExists } from '../core/io.js';
|
|
2
|
-
import {
|
|
2
|
+
import { mutate } from '../core/mutation-pipeline.js';
|
|
3
3
|
import { loadClaim, listClaims, releaseClaim } from '../core/claims.js';
|
|
4
|
-
import {
|
|
4
|
+
import { rebuildProjectMd } from '../core/markdown.js';
|
|
5
5
|
import { loadState, saveState } from '../core/state.js';
|
|
6
6
|
export function runReleaseClaim(id, options = {}) {
|
|
7
7
|
if (!memoryExists(options.cwd)) {
|
|
@@ -10,7 +10,7 @@ export function runReleaseClaim(id, options = {}) {
|
|
|
10
10
|
}
|
|
11
11
|
try {
|
|
12
12
|
let claim = loadClaim(id, options.cwd);
|
|
13
|
-
|
|
13
|
+
mutate({ cwd: options.cwd }, () => {
|
|
14
14
|
const existing = loadClaim(id, options.cwd);
|
|
15
15
|
claim = releaseClaim(id, options.cwd);
|
|
16
16
|
let state = loadState(options.cwd);
|
|
@@ -31,7 +31,7 @@ export function runReleaseClaim(id, options = {}) {
|
|
|
31
31
|
saveState(state, options.cwd);
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
rebuildProjectMd(state, options.cwd);
|
|
35
35
|
});
|
|
36
36
|
console.log(`✔ Claim [${id}] released (was: ${claim.agent} → ${claim.scope})`);
|
|
37
37
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
|
-
import { memoryExists
|
|
2
|
+
import { memoryExists } from '../core/io.js';
|
|
3
|
+
import { mutate } from '../core/mutation-pipeline.js';
|
|
3
4
|
import { listClaims, releaseClaim } from '../core/claims.js';
|
|
4
|
-
import {
|
|
5
|
+
import { rebuildProjectMd } from '../core/markdown.js';
|
|
5
6
|
import { loadState, saveState } from '../core/state.js';
|
|
6
7
|
function normPath(p) {
|
|
7
8
|
return p.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
@@ -36,7 +37,7 @@ export function runReleaseClaims(options = {}) {
|
|
|
36
37
|
if (toRelease.length === 0)
|
|
37
38
|
process.exit(0);
|
|
38
39
|
let released = 0;
|
|
39
|
-
|
|
40
|
+
mutate({ cwd: options.cwd }, () => {
|
|
40
41
|
let state = loadState(options.cwd);
|
|
41
42
|
for (const claim of toRelease) {
|
|
42
43
|
try {
|
|
@@ -59,7 +60,7 @@ export function runReleaseClaims(options = {}) {
|
|
|
59
60
|
catch { /* skip individual failures */ }
|
|
60
61
|
}
|
|
61
62
|
state = loadState(options.cwd);
|
|
62
|
-
|
|
63
|
+
rebuildProjectMd(state, options.cwd);
|
|
63
64
|
});
|
|
64
65
|
if (released > 0) {
|
|
65
66
|
console.log(`brainclaw: ${released} claim(s) auto-released after merge.`);
|
package/dist/commands/switch.js
CHANGED
|
@@ -106,8 +106,8 @@ function listProjects(wsRoot, json) {
|
|
|
106
106
|
});
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
-
// Discover child projects
|
|
110
|
-
const children = scanNestedBrainclawProjects(wsRoot,
|
|
109
|
+
// Discover child projects (depth 7 covers deep workspace layouts like /srv/dev/repos/global/applications/*/...)
|
|
110
|
+
const children = scanNestedBrainclawProjects(wsRoot, 7);
|
|
111
111
|
for (const child of children) {
|
|
112
112
|
const childPath = path.resolve(child.path);
|
|
113
113
|
if (childPath === wsRoot)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { JsonStore } from './json-store.js';
|
|
3
|
-
import { resolveEntityDir
|
|
3
|
+
import { resolveEntityDir } from './io.js';
|
|
4
|
+
import { mutate } from './mutation-pipeline.js';
|
|
4
5
|
import { AiSurfaceTaskRequestSchema } from './schema.js';
|
|
5
6
|
function surfaceTasksDir(cwd, mode = 'read') {
|
|
6
7
|
return resolveEntityDir('surface-tasks', cwd ?? process.cwd(), mode);
|
|
@@ -20,7 +21,7 @@ export function ensureAiSurfaceTasksDir(cwd) {
|
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
23
|
export function saveAiSurfaceTask(task, cwd) {
|
|
23
|
-
|
|
24
|
+
mutate({ cwd }, () => {
|
|
24
25
|
ensureAiSurfaceTasksDir(cwd);
|
|
25
26
|
const writeStore = new JsonStore({
|
|
26
27
|
dirPath: surfaceTasksDir(cwd, 'write'),
|
package/dist/core/audit.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { memoryDir
|
|
3
|
+
import { memoryDir } from './io.js';
|
|
4
|
+
import { mutate } from './mutation-pipeline.js';
|
|
4
5
|
import { nowISO } from './ids.js';
|
|
5
6
|
import { logger } from './logger.js';
|
|
6
7
|
import { appendEvent } from './event-log.js';
|
|
@@ -23,7 +24,7 @@ function auditLogPath(cwd) {
|
|
|
23
24
|
}
|
|
24
25
|
export function appendAuditEntry(entry, cwd) {
|
|
25
26
|
try {
|
|
26
|
-
|
|
27
|
+
mutate({ cwd }, () => {
|
|
27
28
|
const full = {
|
|
28
29
|
timestamp: nowISO(),
|
|
29
30
|
actor: entry.actor,
|
package/dist/core/bootstrap.js
CHANGED
|
@@ -4,7 +4,8 @@ import path from 'node:path';
|
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
5
|
import { JsonStore } from './json-store.js';
|
|
6
6
|
import { generateId, generateIdWithLabel, nowISO } from './ids.js';
|
|
7
|
-
import {
|
|
7
|
+
import { resolveEntityDir } from './io.js';
|
|
8
|
+
import { mutate } from './mutation-pipeline.js';
|
|
8
9
|
import { BootstrapApplicationReceiptSchema, BootstrapInterviewAnswerSchema, BootstrapInterviewPlanSchema, BootstrapInterviewQuestionSchema, BootstrapImportPlanDocumentSchema, BootstrapProfileDocumentSchema, BootstrapSuggestionDocumentSchema, MemorySeedDocumentSchema, } from './schema.js';
|
|
9
10
|
import { loadVersionedJsonFile, saveVersionedJsonFile } from './migration.js';
|
|
10
11
|
import { analyzeRepository } from './repo-analysis.js';
|
|
@@ -13,7 +14,7 @@ import { buildAgentToolingContext } from './agent-context.js';
|
|
|
13
14
|
import { createInstruction, loadInstructions, saveInstruction } from './instructions.js';
|
|
14
15
|
import { resolveCurrentAgentName } from './agent-registry.js';
|
|
15
16
|
import { loadState, persistState } from './state.js';
|
|
16
|
-
import {
|
|
17
|
+
import { rebuildProjectMd } from './markdown.js';
|
|
17
18
|
const README_CANDIDATES = ['README.md', 'README', 'README.txt', 'README.mdx'];
|
|
18
19
|
const DOC_HINTS = ['docs', 'doc'];
|
|
19
20
|
const MAKEFILE_NAME = 'Makefile';
|
|
@@ -1241,7 +1242,7 @@ export function applyBootstrapImport(options = {}) {
|
|
|
1241
1242
|
const managedArtifacts = [];
|
|
1242
1243
|
let createdCount = 0;
|
|
1243
1244
|
let skippedCount = 0;
|
|
1244
|
-
|
|
1245
|
+
mutate({ cwd }, () => {
|
|
1245
1246
|
const state = loadState(cwd);
|
|
1246
1247
|
const activeInstructionKeys = new Set(loadInstructions(cwd)
|
|
1247
1248
|
.filter((entry) => entry.active)
|
|
@@ -1380,7 +1381,7 @@ export function applyBootstrapImport(options = {}) {
|
|
|
1380
1381
|
persistState(state, cwd, { writeProjectMarkdown: false });
|
|
1381
1382
|
}
|
|
1382
1383
|
if (createdCount > 0) {
|
|
1383
|
-
|
|
1384
|
+
rebuildProjectMd(loadState(cwd), cwd);
|
|
1384
1385
|
}
|
|
1385
1386
|
});
|
|
1386
1387
|
const receipt = BootstrapApplicationReceiptSchema.parse({
|
|
@@ -1414,7 +1415,7 @@ export function uninstallBootstrapImport(cwd) {
|
|
|
1414
1415
|
let deactivatedCount = 0;
|
|
1415
1416
|
let deletedCount = 0;
|
|
1416
1417
|
let skippedCount = 0;
|
|
1417
|
-
|
|
1418
|
+
mutate({ cwd: resolvedCwd }, () => {
|
|
1418
1419
|
const state = loadState(resolvedCwd);
|
|
1419
1420
|
const instructions = loadInstructions(resolvedCwd);
|
|
1420
1421
|
let stateChanged = false;
|
|
@@ -1459,7 +1460,7 @@ export function uninstallBootstrapImport(cwd) {
|
|
|
1459
1460
|
persistState(state, resolvedCwd, { writeProjectMarkdown: false });
|
|
1460
1461
|
}
|
|
1461
1462
|
if (deactivatedCount > 0 || deletedCount > 0) {
|
|
1462
|
-
|
|
1463
|
+
rebuildProjectMd(loadState(resolvedCwd), resolvedCwd);
|
|
1463
1464
|
}
|
|
1464
1465
|
});
|
|
1465
1466
|
const nextReceipt = BootstrapApplicationReceiptSchema.parse({
|
package/dist/core/candidates.js
CHANGED
|
@@ -2,7 +2,8 @@ import crypto from 'node:crypto';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { CandidateSchema } from './schema.js';
|
|
5
|
-
import { resolveEntityDir
|
|
5
|
+
import { resolveEntityDir } from './io.js';
|
|
6
|
+
import { mutate } from './mutation-pipeline.js';
|
|
6
7
|
import { nowISO, getNextShortLabel } from './ids.js';
|
|
7
8
|
import { JsonStore } from './json-store.js';
|
|
8
9
|
function inboxDir(cwd, mode = 'read') {
|
|
@@ -35,7 +36,7 @@ function candidateStore(dest = 'pending', cwd) {
|
|
|
35
36
|
});
|
|
36
37
|
}
|
|
37
38
|
export function saveCandidate(candidate, cwd) {
|
|
38
|
-
|
|
39
|
+
mutate({ cwd }, () => {
|
|
39
40
|
ensureInboxDirs(cwd);
|
|
40
41
|
candidateStore('pending', cwd).save(CandidateSchema.parse(candidate));
|
|
41
42
|
});
|
|
@@ -51,7 +52,7 @@ export function listCandidates(status, cwd) {
|
|
|
51
52
|
return status ? candidates.filter((candidate) => candidate.status === status) : candidates;
|
|
52
53
|
}
|
|
53
54
|
export function archiveCandidate(candidate, dest, cwd) {
|
|
54
|
-
|
|
55
|
+
mutate({ cwd }, () => {
|
|
55
56
|
ensureInboxDirs(cwd);
|
|
56
57
|
candidateStore(dest, cwd).save(CandidateSchema.parse(candidate));
|
|
57
58
|
candidateStore('pending', cwd).delete(candidate.id);
|
package/dist/core/claims.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import { ClaimSchema } from './schema.js';
|
|
4
|
-
import { resolveEntityDir
|
|
4
|
+
import { resolveEntityDir } from './io.js';
|
|
5
|
+
import { mutate } from './mutation-pipeline.js';
|
|
5
6
|
import { nowISO } from './ids.js';
|
|
6
7
|
import { JsonStore } from './json-store.js';
|
|
7
8
|
function claimsDir(cwd, mode = 'read') {
|
|
@@ -22,7 +23,7 @@ function claimStore(cwd) {
|
|
|
22
23
|
});
|
|
23
24
|
}
|
|
24
25
|
export function saveClaim(claim, cwd) {
|
|
25
|
-
|
|
26
|
+
mutate({ cwd }, () => {
|
|
26
27
|
ensureClaimsDir(cwd);
|
|
27
28
|
const writeStore = new JsonStore({
|
|
28
29
|
dirPath: claimsDir(cwd, 'write'),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import { resolveEntityDir
|
|
2
|
+
import { resolveEntityDir } from './io.js';
|
|
3
|
+
import { mutate } from './mutation-pipeline.js';
|
|
3
4
|
import { generateId } from './ids.js';
|
|
4
5
|
import { InstructionEntrySchema } from './schema.js';
|
|
5
6
|
import { JsonStore } from './json-store.js';
|
|
@@ -19,7 +20,7 @@ export function loadInstructions(cwd) {
|
|
|
19
20
|
return instructionStore(cwd).list();
|
|
20
21
|
}
|
|
21
22
|
export function saveInstruction(entry, cwd) {
|
|
22
|
-
|
|
23
|
+
mutate({ cwd }, () => {
|
|
23
24
|
instructionStore(cwd, 'write').save(InstructionEntrySchema.parse(entry));
|
|
24
25
|
});
|
|
25
26
|
}
|
package/dist/core/io.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { withLock, cleanStaleLocks } from './lock.js';
|
|
4
|
+
export { mutate } from './mutation-pipeline.js';
|
|
4
5
|
export const MEMORY_DIR = '.brainclaw';
|
|
5
6
|
const STORE_LOCK_BASENAME = '.store-mutation';
|
|
6
7
|
const RETRYABLE_RENAME_ERROR_CODES = new Set(['EPERM', 'EBUSY', 'EACCES']);
|
package/dist/core/lock.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
const DEFAULT_TIMEOUT_MS =
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 5000;
|
|
4
4
|
const LOCK_RETRY_INTERVAL_MS = 50;
|
|
5
|
-
const LOCK_EXPIRY_MS =
|
|
5
|
+
const LOCK_EXPIRY_MS = 10000;
|
|
6
6
|
const heldLocks = new Map();
|
|
7
7
|
function lockFilePath(targetPath) {
|
|
8
8
|
return targetPath + '.lock';
|
package/dist/core/markdown.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { listClaims } from './claims.js';
|
|
2
2
|
import { loadInstructions } from './instructions.js';
|
|
3
|
+
import { memoryPath, writeFileAtomic } from './io.js';
|
|
3
4
|
import { isTrapActive } from './traps.js';
|
|
5
|
+
import { logger } from './logger.js';
|
|
4
6
|
export function generateMarkdown(state, cwd) {
|
|
5
7
|
const lines = ['# Project Memory', ''];
|
|
6
8
|
const instructions = loadInstructions(cwd).filter((entry) => entry.active);
|
|
@@ -117,4 +119,20 @@ export function generateMarkdown(state, cwd) {
|
|
|
117
119
|
lines.push('');
|
|
118
120
|
return lines.join('\n');
|
|
119
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Rebuild `.brainclaw/project.md` from canonical state.
|
|
124
|
+
*
|
|
125
|
+
* This is a **derived view** — it can always be regenerated from the
|
|
126
|
+
* canonical JSON files. Call this once at the end of a top-level mutation,
|
|
127
|
+
* not inside every nested helper. Best-effort: failures are logged but
|
|
128
|
+
* never propagate to the caller.
|
|
129
|
+
*/
|
|
130
|
+
export function rebuildProjectMd(state, cwd) {
|
|
131
|
+
try {
|
|
132
|
+
writeFileAtomic(memoryPath('project.md', cwd), generateMarkdown(state, cwd));
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
logger.debug('Failed to rebuild project.md:', err);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
120
138
|
//# sourceMappingURL=markdown.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutation pipeline — single entry point for all .brainclaw/ store mutations.
|
|
3
|
+
*
|
|
4
|
+
* Every write operation against the store MUST go through `mutate()`.
|
|
5
|
+
* This ensures:
|
|
6
|
+
* 1. Store-wide serialization via the advisory file lock
|
|
7
|
+
* 2. Consistent error handling and timeout behavior
|
|
8
|
+
* 3. Observable mutation metadata for debugging and auditing
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import { ensureMemoryDir, storeLockPath } from './io.js';
|
|
13
|
+
import { withLock } from './lock.js';
|
|
14
|
+
import { logger } from './logger.js';
|
|
15
|
+
/** Default timeout for store-wide lock acquisition (ms). */
|
|
16
|
+
export const STORE_LOCK_TIMEOUT_MS = 5_000;
|
|
17
|
+
export function mutate(optionsOrFn, maybeFn) {
|
|
18
|
+
const options = typeof optionsOrFn === 'function' ? {} : optionsOrFn;
|
|
19
|
+
const fn = typeof optionsOrFn === 'function' ? optionsOrFn : maybeFn;
|
|
20
|
+
const cwd = options.cwd ?? process.cwd();
|
|
21
|
+
const timeoutMs = options.timeoutMs ?? STORE_LOCK_TIMEOUT_MS;
|
|
22
|
+
ensureMemoryDir(cwd, options.preferredDirName);
|
|
23
|
+
const lockTarget = storeLockPath(cwd, options.preferredDirName);
|
|
24
|
+
const start = performance.now();
|
|
25
|
+
try {
|
|
26
|
+
const value = withLock(lockTarget, () => fn(cwd), timeoutMs);
|
|
27
|
+
const durationMs = performance.now() - start;
|
|
28
|
+
if (durationMs > 1_000) {
|
|
29
|
+
logger.debug(`Slow mutation: ${durationMs.toFixed(0)}ms (cwd=${cwd})`);
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
const durationMs = performance.now() - start;
|
|
35
|
+
logger.debug(`Mutation failed after ${durationMs.toFixed(0)}ms: ${err instanceof Error ? err.message : String(err)}`);
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=mutation-pipeline.js.map
|
package/dist/core/runtime.js
CHANGED
|
@@ -2,7 +2,8 @@ import crypto from 'node:crypto';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { resolveCurrentHostId, sanitizeHostId } from './host.js';
|
|
5
|
-
import { resolveEntityDir
|
|
5
|
+
import { resolveEntityDir } from './io.js';
|
|
6
|
+
import { mutate } from './mutation-pipeline.js';
|
|
6
7
|
import { loadVersionedJsonFile, saveVersionedJsonFile } from './migration.js';
|
|
7
8
|
import { RuntimeNoteSchema } from './schema.js';
|
|
8
9
|
import { commitMemoryChange } from './memory-git.js';
|
|
@@ -40,7 +41,7 @@ export function saveRuntimeNote(note, cwd) {
|
|
|
40
41
|
const persistedNote = visibility === 'shared'
|
|
41
42
|
? { ...note, visibility, host_id: hostId }
|
|
42
43
|
: { ...note, visibility, host_id: hostId };
|
|
43
|
-
|
|
44
|
+
mutate({ cwd }, () => {
|
|
44
45
|
ensureRuntimeDir(note.agent, cwd, visibility, hostId);
|
|
45
46
|
const filepath = visibility === 'shared'
|
|
46
47
|
? path.join(sharedAgentDir(note.agent, cwd, 'write'), `${note.id}.json`)
|
|
@@ -58,7 +59,7 @@ export function runtimeNotePath(note, cwd) {
|
|
|
58
59
|
: path.join(hostAgentDir(visibility, hostId, note.agent, cwd), `${note.id}.json`);
|
|
59
60
|
}
|
|
60
61
|
export function deleteRuntimeNote(note, cwd) {
|
|
61
|
-
return
|
|
62
|
+
return mutate({ cwd }, () => {
|
|
62
63
|
const filepath = runtimeNotePath(note, cwd);
|
|
63
64
|
if (!fs.existsSync(filepath)) {
|
|
64
65
|
return false;
|
package/dist/core/state.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { ConstraintSchema, DecisionSchema, TrapSchema, HandoffSchema, PlanItemSchema } from './schema.js';
|
|
4
|
-
import { ensureMemoryDir,
|
|
4
|
+
import { ensureMemoryDir, resolveEntityDir } from './io.js';
|
|
5
|
+
import { mutate } from './mutation-pipeline.js';
|
|
5
6
|
import { commitMemoryChange } from './memory-git.js';
|
|
6
7
|
import { appendEvent } from './event-log.js';
|
|
7
8
|
import { loadVersionedJsonFile, saveVersionedJsonFile } from './migration.js';
|
|
8
|
-
import {
|
|
9
|
+
import { rebuildProjectMd } from './markdown.js';
|
|
9
10
|
export function emptyState() {
|
|
10
11
|
return {
|
|
11
12
|
version: 1,
|
|
@@ -83,10 +84,10 @@ function writeStateDirectories(state, cwd) {
|
|
|
83
84
|
}
|
|
84
85
|
export function persistState(state, cwd, options = {}) {
|
|
85
86
|
const effectiveCwd = cwd ?? process.cwd();
|
|
86
|
-
|
|
87
|
+
mutate({ cwd: effectiveCwd }, () => {
|
|
87
88
|
writeStateDirectories(state, effectiveCwd);
|
|
88
89
|
if (options.writeProjectMarkdown ?? true) {
|
|
89
|
-
|
|
90
|
+
rebuildProjectMd(state, effectiveCwd);
|
|
90
91
|
}
|
|
91
92
|
appendEvent({
|
|
92
93
|
action: options.eventAction ?? 'update',
|
|
@@ -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
|
package/docs/concepts/memory.md
CHANGED
|
@@ -32,7 +32,7 @@ Examples: constraints, decisions, traps, completed plans, handoffs.
|
|
|
32
32
|
|
|
33
33
|
Shared traps now have a lifecycle too: `active`, `resolved`, or `expired`. Active views such as generated context, status, and `project.md` prioritize only active traps so old machine-setup issues stop polluting the current working set, while the canonical memory still keeps resolved traps for audit and search.
|
|
34
34
|
|
|
35
|
-
These live
|
|
35
|
+
These live as individual JSON files under `.brainclaw/memory/` (constraints, decisions, traps, instructions) and `.brainclaw/coordination/` (plans, claims, handoffs). They are versioned in `.brainclaw/.git` and shared via Git.
|
|
36
36
|
|
|
37
37
|
### Runtime memory
|
|
38
38
|
Operational observations that may be short-lived, host-specific, or private.
|
|
@@ -59,8 +59,9 @@ brainclaw makes this context visible and versionable.
|
|
|
59
59
|
|
|
60
60
|
brainclaw keeps:
|
|
61
61
|
|
|
62
|
-
- canonical structured JSON as the source of truth (`.brainclaw/
|
|
63
|
-
- a
|
|
62
|
+
- canonical structured JSON as the source of truth (individual files under `.brainclaw/memory/` and `.brainclaw/coordination/`)
|
|
63
|
+
- a derived readable view (`project.md`) regenerated best-effort from canonical state
|
|
64
|
+
- native agent instruction files (`CLAUDE.md`, `.cursor/rules/brainclaw.md`, etc.) generated via `brainclaw export`
|
|
64
65
|
|
|
65
66
|
This balances machine reliability with human readability.
|
|
66
67
|
|
|
@@ -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
|
|
@@ -14,7 +14,7 @@ brainclaw export --format claude-md --write
|
|
|
14
14
|
|
|
15
15
|
- use MCP as the default runtime path for dynamic retrieval and writes
|
|
16
16
|
- keep `CLAUDE.md` lightweight and behavioral: it should tell Claude Code when to call Brainclaw, not carry all mutable workspace state
|
|
17
|
-
- use `.brainclaw/project.md` as a readable fallback
|
|
17
|
+
- use `.brainclaw/project.md` as a readable fallback (it is a derived view, regenerated best-effort — run `brainclaw rebuild` if stale)
|
|
18
18
|
- use hooks or workflow checks when a stronger reminder is needed
|
|
19
19
|
|
|
20
20
|
## Key idea
|
|
@@ -14,7 +14,7 @@ brainclaw export --format agents-md --write
|
|
|
14
14
|
|
|
15
15
|
- use MCP as the default runtime path for fresh context, plans, claims, and runtime notes
|
|
16
16
|
- keep `AGENTS.md` lightweight and behavioral: it should remind Codex how to use Brainclaw, not duplicate live state
|
|
17
|
-
- use `.brainclaw/project.md` only as a readable fallback
|
|
17
|
+
- use `.brainclaw/project.md` only as a readable fallback (derived view, regenerated best-effort — run `brainclaw rebuild` if stale)
|
|
18
18
|
- encourage plans, claims, and handoffs during multi-step work
|
|
19
19
|
|
|
20
20
|
## Good role for brainclaw here
|
|
@@ -14,7 +14,7 @@ brainclaw export --format copilot-instructions --write
|
|
|
14
14
|
|
|
15
15
|
- use MCP whenever the Copilot surface supports it for fresh context and coordination views
|
|
16
16
|
- keep `.github/copilot-instructions.md` lightweight and behavioral
|
|
17
|
-
- use `.brainclaw/project.md` as readable fallback,
|
|
17
|
+
- use `.brainclaw/project.md` as readable fallback (derived view, regenerated best-effort — may need `brainclaw rebuild` if stale)
|
|
18
18
|
- use plans, claims, and handoffs to reduce ambiguity across sessions
|
|
19
19
|
|
|
20
20
|
## Why this matters
|
|
@@ -14,7 +14,7 @@ brainclaw export --format cursor-rules --write
|
|
|
14
14
|
|
|
15
15
|
- use MCP as the default dynamic path for context, board state, plans, and claims
|
|
16
16
|
- let the generated `.cursor/rules/brainclaw.md` tell Cursor when to consult Brainclaw and how to stay inside the workflow
|
|
17
|
-
- use `.brainclaw/project.md` only as readable fallback
|
|
17
|
+
- use `.brainclaw/project.md` only as readable fallback (derived view, may be stale — run `brainclaw rebuild` to refresh)
|
|
18
18
|
- rely on claims and plans when multiple agents or humans are active in the same repo
|
|
19
19
|
|
|
20
20
|
## Key point
|
package/docs/integrations/mcp.md
CHANGED
|
@@ -58,7 +58,7 @@ This keeps session continuity inside Brainclaw instead of pushing the agent back
|
|
|
58
58
|
| Runtime writes with session continuity | MCP |
|
|
59
59
|
| Local behavioral reminders inside the agent UI | native agent files |
|
|
60
60
|
| Human inspection or scripting | CLI |
|
|
61
|
-
| Simple readable fallback | `.brainclaw/project.md` |
|
|
61
|
+
| Simple readable fallback | `.brainclaw/project.md` (derived view, may be stale) |
|
|
62
62
|
|
|
63
63
|
## Starting The Server
|
|
64
64
|
|
|
@@ -87,16 +87,26 @@ Interview answers are keyed by question ID and may contain:
|
|
|
87
87
|
- `response_boolean`
|
|
88
88
|
- optional explicit `suggestions` when the agent wants to confirm exact canonical memory items
|
|
89
89
|
|
|
90
|
+
## Mutation Safety
|
|
91
|
+
|
|
92
|
+
The MCP server serializes all mutations through a single-writer queue (`McpTaskRunner`). When an agent calls a write tool (e.g. `bclaw_claim`, `bclaw_write_note`, `bclaw_create_plan`), the request is enqueued and executed one at a time. This guarantees:
|
|
93
|
+
|
|
94
|
+
- no concurrent writes from the same MCP connection
|
|
95
|
+
- no partial state from interleaved mutations
|
|
96
|
+
- deterministic ordering of operations
|
|
97
|
+
|
|
98
|
+
A secondary file-based lock (`mutate()`) provides cross-process safety in case CLI commands run alongside MCP. But for agents, MCP is the safe path by design — no extra precautions needed.
|
|
99
|
+
|
|
90
100
|
## Important Rule
|
|
91
101
|
|
|
92
|
-
If the agent has MCP available, do not treat the CLI as the primary runtime interface.
|
|
102
|
+
If the agent has MCP available, do not treat the CLI as the primary runtime interface. All agent mutations MUST go through MCP tools.
|
|
93
103
|
|
|
94
104
|
The CLI remains valuable for:
|
|
95
105
|
|
|
96
|
-
- setup
|
|
106
|
+
- setup and initialization
|
|
97
107
|
- bootstrap by a human operator
|
|
98
|
-
- scripting
|
|
108
|
+
- scripting and automation
|
|
99
109
|
- release and packaging
|
|
100
110
|
- debugging and fallback access
|
|
101
111
|
|
|
102
|
-
But for capable agents, MCP
|
|
112
|
+
But for capable agents, MCP is the first-class path for both reads and writes.
|
|
@@ -87,9 +87,9 @@ The developer can dial back individual surfaces if needed, but the default is fu
|
|
|
87
87
|
|
|
88
88
|
## Sequential collaboration, not parallel editing
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
brainclaw's store mutations are serialized (MCP single-writer queue + file-based lock), so memory writes are safe even under contention. However, running multiple agents in parallel on the same checkout can still cause Git conflicts and confusing file-level state.
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
For now, brainclaw works best when one agent works at a time in a given checkout. The next agent can pick up where the previous one stopped, using shared plans, claims, handoffs, and memory. Git worktree isolation per agent is planned but not yet available.
|
|
93
93
|
|
|
94
94
|
## Next reads
|
|
95
95
|
|
package/docs/quickstart.md
CHANGED
|
@@ -74,7 +74,9 @@ This keeps non-code work visible to the project without overloading the active c
|
|
|
74
74
|
|
|
75
75
|
## Important: one agent at a time
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
brainclaw serializes all store mutations (file lock + MCP single-writer queue), so writes are safe. But running multiple agents in parallel on the same checkout can still cause Git conflicts and confusing state transitions.
|
|
78
|
+
|
|
79
|
+
Use brainclaw for sequential collaboration: one agent works, finishes, and the next one picks up from shared context. Use `bclaw_session_end` to hand off cleanly.
|
|
78
80
|
|
|
79
81
|
## Next reads
|
|
80
82
|
|
package/docs/storage.md
CHANGED
|
@@ -6,37 +6,62 @@ brainclaw is local-first and workspace-centric.
|
|
|
6
6
|
|
|
7
7
|
```text
|
|
8
8
|
.brainclaw/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
config.yaml ← Project configuration
|
|
10
|
+
project.md ← Derived readable view (best-effort, regenerable)
|
|
11
|
+
events.jsonl ← Append-only event log (agent notifications)
|
|
12
|
+
audit.log ← Append-only audit trail (before/after snapshots)
|
|
13
|
+
memory/
|
|
14
|
+
constraints/ ← Canonical constraint entries (one JSON per entity)
|
|
15
|
+
decisions/ ← Canonical decision entries
|
|
16
|
+
traps/ ← Canonical trap entries
|
|
17
|
+
instructions/ ← Layered shared instructions
|
|
18
|
+
coordination/
|
|
19
|
+
plans/ ← Shared plan items
|
|
20
|
+
claims/ ← Active scope claims
|
|
21
|
+
handoffs/ ← Handoff records
|
|
22
|
+
sessions/ ← Session state
|
|
23
|
+
runtime/ ← Runtime notes (shared, machine, private)
|
|
24
|
+
inbox/ ← Candidate review queue
|
|
25
|
+
discovery/
|
|
26
|
+
bootstrap/ ← Bootstrap profiles and seeds
|
|
27
|
+
agents/ ← Agent registration and identity
|
|
17
28
|
```
|
|
18
29
|
|
|
19
30
|
## Design principles
|
|
20
31
|
|
|
21
32
|
### Canonical state is split
|
|
22
|
-
Each entity is stored as its own JSON file.
|
|
33
|
+
Each entity is stored as its own JSON file (e.g. `memory/decisions/dec_abc123.json`).
|
|
23
34
|
|
|
24
35
|
Benefits:
|
|
25
36
|
|
|
26
|
-
- readable diffs
|
|
37
|
+
- readable diffs in `.brainclaw/.git`
|
|
27
38
|
- easier merges
|
|
28
|
-
- clear provenance
|
|
29
|
-
-
|
|
39
|
+
- clear provenance per entity
|
|
40
|
+
- O(1) lookup by ID (direct file access)
|
|
30
41
|
- no giant monolithic memory blob
|
|
42
|
+
- isolated corruption (one bad file does not affect others)
|
|
31
43
|
|
|
32
|
-
###
|
|
33
|
-
`project.md` is regenerated from canonical state
|
|
44
|
+
### Derived views are best-effort
|
|
45
|
+
`project.md` is a **derived view** regenerated from canonical state via `rebuildProjectMd()`. It is not a write target — it can always be rebuilt from the JSON files using `brainclaw rebuild`.
|
|
34
46
|
|
|
35
|
-
|
|
47
|
+
Failures to regenerate `project.md` are logged but never block mutations. If it gets stale, `brainclaw doctor` detects the drift.
|
|
48
|
+
|
|
49
|
+
### All mutations go through a single pipeline
|
|
50
|
+
Every write to `.brainclaw/` is serialized through `mutate()` (`src/core/mutation-pipeline.ts`):
|
|
51
|
+
|
|
52
|
+
1. **Store-wide lock** — advisory file lock at `.brainclaw/.store-mutation` (5s timeout, 10s expiry)
|
|
53
|
+
2. **Reentrant** — nested mutations within the same process are safe
|
|
54
|
+
3. **Atomic writes** — individual files use temp-file + rename pattern
|
|
55
|
+
4. **Git versioned** — `.brainclaw/.git` tracks all changes for rollback
|
|
56
|
+
|
|
57
|
+
For agents using MCP (the recommended path), mutations are additionally serialized by the MCP server's `McpTaskRunner` — a FIFO queue that runs one task at a time. This provides two layers of write safety:
|
|
58
|
+
|
|
59
|
+
| Layer | Scope | Mechanism |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| **McpTaskRunner** | Single MCP server process | FIFO queue, one active Worker thread |
|
|
62
|
+
| **mutate() file lock** | Cross-process (CLI + MCP) | Advisory `.store-mutation` lock file |
|
|
36
63
|
|
|
37
|
-
|
|
38
|
-
- humans get an inspectable summary
|
|
39
|
-
- the source of truth remains structured
|
|
64
|
+
Agents should always use MCP tools for mutations. The CLI is for human operators, scripting, and fallback workflows.
|
|
40
65
|
|
|
41
66
|
### Topology can vary
|
|
42
67
|
|
|
@@ -57,21 +82,23 @@ Depending on configuration, storage may be:
|
|
|
57
82
|
- handoffs
|
|
58
83
|
- plans
|
|
59
84
|
|
|
60
|
-
## What
|
|
85
|
+
## What stays operational
|
|
61
86
|
|
|
62
87
|
- machine-local runtime notes
|
|
63
88
|
- private notes
|
|
64
|
-
- short-lived observations
|
|
89
|
+
- short-lived observations (with optional TTL)
|
|
65
90
|
- reflective candidates awaiting review
|
|
91
|
+
- event log and audit trail
|
|
66
92
|
|
|
67
93
|
## Why this model matters
|
|
68
94
|
|
|
69
95
|
The storage model is part of the product value:
|
|
70
96
|
|
|
71
|
-
- local-first
|
|
72
|
-
- inspectable
|
|
73
|
-
- Git-friendly
|
|
74
|
-
- reversible
|
|
97
|
+
- local-first — no cloud dependency
|
|
98
|
+
- inspectable — plain text + JSON
|
|
99
|
+
- Git-friendly — entity-per-file, readable diffs
|
|
100
|
+
- reversible — `.brainclaw/.git` history + rollback
|
|
101
|
+
- mutation-safe — serialized writes, atomic file operations
|
|
75
102
|
- suitable for both humans and agents
|
|
76
103
|
|
|
77
104
|
## Related pages
|