brainclaw 0.23.1 → 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/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/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/concepts/memory.md +4 -3
- 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/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.`);
|
|
@@ -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',
|
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
|
|
|
@@ -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
|