brainclaw 0.19.6 → 0.19.10

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.
Files changed (51) hide show
  1. package/README.md +225 -126
  2. package/dist/cli.js +8 -3
  3. package/dist/commands/accept.js +102 -104
  4. package/dist/commands/add-step.js +3 -5
  5. package/dist/commands/bootstrap.js +72 -3
  6. package/dist/commands/capability.js +3 -5
  7. package/dist/commands/claim.js +14 -12
  8. package/dist/commands/complete-step.js +3 -5
  9. package/dist/commands/constraint.js +3 -5
  10. package/dist/commands/decision.js +3 -6
  11. package/dist/commands/delete-plan.js +3 -5
  12. package/dist/commands/handoff.js +3 -5
  13. package/dist/commands/init.js +20 -0
  14. package/dist/commands/instruction.js +16 -9
  15. package/dist/commands/mcp.js +18 -22
  16. package/dist/commands/memory.js +4 -7
  17. package/dist/commands/plan.js +3 -5
  18. package/dist/commands/prune.js +27 -25
  19. package/dist/commands/reflect.js +3 -5
  20. package/dist/commands/release-claim.js +23 -20
  21. package/dist/commands/release-claims.js +22 -21
  22. package/dist/commands/rollback.js +2 -2
  23. package/dist/commands/tool.js +3 -5
  24. package/dist/commands/trap.js +3 -5
  25. package/dist/commands/update-handoff.js +3 -5
  26. package/dist/commands/update-plan.js +3 -5
  27. package/dist/commands/upgrade.js +2 -2
  28. package/dist/core/audit.js +25 -25
  29. package/dist/core/bootstrap.js +587 -4
  30. package/dist/core/candidates.js +10 -6
  31. package/dist/core/claims.js +10 -8
  32. package/dist/core/coordination.js +2 -2
  33. package/dist/core/instructions.js +4 -2
  34. package/dist/core/io.js +8 -0
  35. package/dist/core/lock.js +18 -2
  36. package/dist/core/migration.js +6 -2
  37. package/dist/core/runtime.js +18 -14
  38. package/dist/core/schema.js +69 -0
  39. package/dist/core/state.js +21 -4
  40. package/docs/cli.md +21 -1
  41. package/docs/integrations/agents.md +42 -29
  42. package/docs/integrations/claude-code.md +4 -4
  43. package/docs/integrations/codex.md +5 -5
  44. package/docs/integrations/copilot.md +3 -2
  45. package/docs/integrations/cursor.md +4 -4
  46. package/docs/integrations/mcp.md +53 -24
  47. package/docs/integrations/overview.md +53 -40
  48. package/docs/mcp-schema-changelog.md +3 -0
  49. package/docs/product/positioning.md +17 -18
  50. package/docs/quickstart.md +87 -55
  51. package/package.json +1 -2
@@ -8,9 +8,8 @@ import { buildCoordinationSnapshot } from '../core/coordination.js';
8
8
  import { buildContext, renderContextMarkdown, renderContextPromptTemplate } from '../core/context.js';
9
9
  import { buildExecutionContext, renderExecutionContextSummary } from '../core/execution-context.js';
10
10
  import { loadConfig } from '../core/config.js';
11
- import { loadState, saveState } from '../core/state.js';
12
- import { memoryExists, memoryPath, writeFileAtomic } from '../core/io.js';
13
- import { generateMarkdown } from '../core/markdown.js';
11
+ import { loadState, persistState } from '../core/state.js';
12
+ import { memoryExists } from '../core/io.js';
14
13
  import { generateCandidateIdWithLabel, listArchivedCandidates, listCandidates, saveCandidate } from '../core/candidates.js';
15
14
  import { generateClaimId, listClaims, loadClaim, saveClaim } from '../core/claims.js';
16
15
  import { createRuntimeNote } from './runtime-note.js';
@@ -62,7 +61,7 @@ export const MCP_READ_TOOLS = [
62
61
  },
63
62
  {
64
63
  name: 'bclaw_bootstrap',
65
- description: 'Derive brownfield bootstrap signals from repository docs, manifests, and git history.',
64
+ description: 'Derive brownfield bootstrap signals, adaptive interview prompts for CLI or IDE chat agents, and an import proposal from repository docs, manifests, native agent files, and git history.',
66
65
  inputSchema: {
67
66
  type: 'object',
68
67
  properties: {
@@ -1053,8 +1052,14 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1053
1052
  target: result.profile.target,
1054
1053
  repo_fingerprint: result.profile.repo_fingerprint,
1055
1054
  sources_scanned: result.profile.sources_scanned,
1055
+ workspace_kind: result.profile.workspace_kind,
1056
+ confidence: result.profile.confidence,
1057
+ native_instruction_files: result.profile.native_instruction_files,
1058
+ gaps: result.profile.gaps,
1056
1059
  seed_count: result.profile.seed_count,
1057
1060
  seeds: result.seeds,
1061
+ import_plan: result.importPlan,
1062
+ last_application: result.lastApplication,
1058
1063
  reused_profile: result.reusedProfile,
1059
1064
  },
1060
1065
  };
@@ -1808,8 +1813,7 @@ export async function executeMcpToolCall(payload) {
1808
1813
  if (releasePlanStatus === 'done' && !releasePlan.completed_at)
1809
1814
  releasePlan.completed_at = ts;
1810
1815
  releasePlan.updated_at = ts;
1811
- saveState(releaseState, cwd);
1812
- writeFileAtomic(memoryPath('project.md', cwd), generateMarkdown(releaseState, cwd));
1816
+ persistState(releaseState, cwd);
1813
1817
  releasePlanUpdated = true;
1814
1818
  }
1815
1819
  }
@@ -1957,8 +1961,7 @@ export async function executeMcpToolCall(payload) {
1957
1961
  estimated_effort: estimatedEffort,
1958
1962
  };
1959
1963
  state.plan_items.push(entry);
1960
- saveState(state, cwd);
1961
- writeFileAtomic(memoryPath('project.md', cwd), generateMarkdown(state, cwd));
1964
+ persistState(state, cwd);
1962
1965
  appendAuditEntry({ actor: resolved.identity.agent_name, actor_id: resolved.identity.agent_id, action: 'create', item_id: id, item_type: 'plan' }, cwd);
1963
1966
  return {
1964
1967
  response: toolResponse({
@@ -1996,8 +1999,7 @@ export async function executeMcpToolCall(payload) {
1996
1999
  if (args.actualEffort)
1997
2000
  plan.actual_effort = args.actualEffort;
1998
2001
  plan.updated_at = timestamp;
1999
- saveState(state, cwd);
2000
- writeFileAtomic(memoryPath('project.md', cwd), generateMarkdown(state, cwd));
2002
+ persistState(state, cwd);
2001
2003
  appendAuditEntry({ actor: resolved.identity.agent_name, actor_id: resolved.identity.agent_id, action: 'update', item_id: plan.id, item_type: 'plan' }, cwd);
2002
2004
  return {
2003
2005
  response: toolResponse({
@@ -2032,8 +2034,7 @@ export async function executeMcpToolCall(payload) {
2032
2034
  };
2033
2035
  plan.steps = [...(plan.steps ?? []), step];
2034
2036
  plan.updated_at = nowISO();
2035
- saveState(state, cwd);
2036
- writeFileAtomic(memoryPath('project.md', cwd), generateMarkdown(state, cwd));
2037
+ persistState(state, cwd);
2037
2038
  const total = plan.steps.length;
2038
2039
  const done = plan.steps.filter((s) => s.status === 'done').length;
2039
2040
  return {
@@ -2066,8 +2067,7 @@ export async function executeMcpToolCall(payload) {
2066
2067
  step.status = 'done';
2067
2068
  step.updated_at = nowISO();
2068
2069
  plan.updated_at = nowISO();
2069
- saveState(state, cwd);
2070
- writeFileAtomic(memoryPath('project.md', cwd), generateMarkdown(state, cwd));
2070
+ persistState(state, cwd);
2071
2071
  const total = plan.steps.length;
2072
2072
  const done = plan.steps.filter((s) => s.status === 'done').length;
2073
2073
  return {
@@ -2124,8 +2124,7 @@ export async function executeMcpToolCall(payload) {
2124
2124
  else if (itemType === 'trap') {
2125
2125
  state.known_traps = state.known_traps.filter((t) => t.id !== itemId && t.short_label !== itemId);
2126
2126
  }
2127
- saveState(state, foundStore.cwd);
2128
- writeFileAtomic(memoryPath('project.md', foundStore.cwd), generateMarkdown(state, foundStore.cwd));
2127
+ persistState(state, foundStore.cwd);
2129
2128
  appendAuditEntry({ actor: resolved.identity.agent_name, actor_id: resolved.identity.agent_id, action: 'delete', item_id: itemId, item_type: itemType }, foundStore.cwd);
2130
2129
  return {
2131
2130
  response: toolResponse({
@@ -2211,8 +2210,7 @@ export async function executeMcpToolCall(payload) {
2211
2210
  else if (itemType === 'trap') {
2212
2211
  sourceState.known_traps = sourceState.known_traps.filter((t) => t.id !== itemId);
2213
2212
  }
2214
- saveState(sourceState, sourceStore.cwd);
2215
- writeFileAtomic(memoryPath('project.md', sourceStore.cwd), generateMarkdown(sourceState, sourceStore.cwd));
2213
+ persistState(sourceState, sourceStore.cwd);
2216
2214
  // Add to target store
2217
2215
  const targetState = loadState(targetCwd);
2218
2216
  if (itemType === 'constraint') {
@@ -2224,8 +2222,7 @@ export async function executeMcpToolCall(payload) {
2224
2222
  else if (itemType === 'trap') {
2225
2223
  targetState.known_traps.push(item);
2226
2224
  }
2227
- saveState(targetState, targetCwd);
2228
- writeFileAtomic(memoryPath('project.md', targetCwd), generateMarkdown(targetState, targetCwd));
2225
+ persistState(targetState, targetCwd);
2229
2226
  }
2230
2227
  else {
2231
2228
  // Just update in place
@@ -2245,8 +2242,7 @@ export async function executeMcpToolCall(payload) {
2245
2242
  if (idx >= 0)
2246
2243
  state.known_traps[idx] = item;
2247
2244
  }
2248
- saveState(state, sourceStore.cwd);
2249
- writeFileAtomic(memoryPath('project.md', sourceStore.cwd), generateMarkdown(state, sourceStore.cwd));
2245
+ persistState(state, sourceStore.cwd);
2250
2246
  }
2251
2247
  appendAuditEntry({ actor: resolved.identity.agent_name, actor_id: resolved.identity.agent_id, action: 'update', item_id: itemId, item_type: itemType }, sourceStore.cwd);
2252
2248
  return {
@@ -1,7 +1,6 @@
1
1
  import { loadConfig } from '../core/config.js';
2
- import { memoryExists, memoryPath, writeFileAtomic } from '../core/io.js';
3
- import { generateMarkdown } from '../core/markdown.js';
4
- import { loadState, saveState } from '../core/state.js';
2
+ import { memoryExists } from '../core/io.js';
3
+ import { loadState, persistState } from '../core/state.js';
5
4
  import { scanText } from '../core/security.js';
6
5
  import { validateCliInput } from '../core/input-validation.js';
7
6
  import { resolveTargetStore } from '../core/store-resolution.js';
@@ -222,8 +221,7 @@ function runMemoryUpdate(id, options) {
222
221
  break;
223
222
  }
224
223
  }
225
- saveState(state, options.cwd);
226
- writeFileAtomic(memoryPath('project.md', options.cwd), generateMarkdown(state, options.cwd));
224
+ persistState(state, options.cwd);
227
225
  console.log(`✔ Memory item updated: [${resolved.item.id}] ${resolved.item.text}`);
228
226
  }
229
227
  function runMemoryDelete(id, cwd) {
@@ -250,8 +248,7 @@ function runMemoryDelete(id, cwd) {
250
248
  console.error(`Error: Memory item '${id}' not found.`);
251
249
  process.exit(1);
252
250
  }
253
- saveState(state, cwd);
254
- writeFileAtomic(memoryPath('project.md', cwd), generateMarkdown(state, cwd));
251
+ persistState(state, cwd);
255
252
  console.log(`✔ Memory item deleted: [${deleted.id}] ${deleted.text}`);
256
253
  }
257
254
  function collectMemoryItems(state) {
@@ -1,10 +1,9 @@
1
- import { loadState, saveState } from '../core/state.js';
1
+ import { loadState, persistState } from '../core/state.js';
2
2
  import { resolveCurrentAgentName } from '../core/agent-registry.js';
3
3
  import { loadConfig } from '../core/config.js';
4
- import { generateMarkdown } from '../core/markdown.js';
5
4
  import { generateIdWithLabel, nowISO } from '../core/ids.js';
6
5
  import { scanText } from '../core/security.js';
7
- import { memoryExists, memoryPath, writeFileAtomic } from '../core/io.js';
6
+ import { memoryExists } from '../core/io.js';
8
7
  import { validateCliInput } from '../core/input-validation.js';
9
8
  import { runListPlans } from './list-plans.js';
10
9
  import { resolveTargetStore } from '../core/store-resolution.js';
@@ -68,8 +67,7 @@ export function runPlan(text, options = {}) {
68
67
  estimated_effort: estimatedEffort,
69
68
  };
70
69
  state.plan_items.push(entry);
71
- saveState(state, cwd);
72
- writeFileAtomic(memoryPath('project.md', cwd), generateMarkdown(state, cwd));
70
+ persistState(state, cwd);
73
71
  const storeLabel = options.store && options.store !== 'local' ? ` [store:${options.store}]` : '';
74
72
  console.log(`✔ Plan item added: [${id}] ${text}${storeLabel}`);
75
73
  }
@@ -1,5 +1,6 @@
1
1
  import { loadState, saveState } from '../core/state.js';
2
- import { memoryExists } from '../core/io.js';
2
+ import { memoryExists, memoryPath, withStoreLock, writeFileAtomic } from '../core/io.js';
3
+ import { generateMarkdown } from '../core/markdown.js';
3
4
  import { deleteRuntimeNote, listRuntimeNotes } from '../core/runtime.js';
4
5
  import { expireStaleActiveClaims } from '../core/claims.js';
5
6
  export function runPrune(options = {}) {
@@ -8,36 +9,37 @@ export function runPrune(options = {}) {
8
9
  console.error('Project memory not initialized. Run `brainclaw init` first.');
9
10
  process.exit(1);
10
11
  }
11
- const state = loadState(cwd);
12
12
  const now = new Date().toISOString();
13
- const originalLength = state.active_constraints.length;
14
- // Transition expired constraints to "expired"
15
- for (const c of state.active_constraints) {
16
- if (c.status === 'active' && c.expires_at && c.expires_at < now) {
17
- c.status = 'expired';
18
- }
19
- }
20
- // Filter out constraints that have been expired for a while to keep memory clean
21
- // For the MVP, we just remove them if they are expired
22
- state.active_constraints = state.active_constraints.filter(c => c.status !== 'expired');
23
- const prunedCount = originalLength - state.active_constraints.length;
24
- saveState(state, cwd);
25
- const expiredClaimsCount = expireStaleActiveClaims(cwd);
13
+ let prunedCount = 0;
14
+ let expiredClaimsCount = 0;
26
15
  let expiredNotesCount = 0;
27
- if (options.expired) {
28
- // Prune expired runtime notes
29
- const notes = listRuntimeNotes(undefined, cwd);
30
- for (const note of notes) {
31
- if (note.expires_at && note.expires_at < now) {
32
- try {
33
- if (deleteRuntimeNote(note, cwd)) {
34
- expiredNotesCount++;
16
+ withStoreLock(cwd, () => {
17
+ const state = loadState(cwd);
18
+ const originalLength = state.active_constraints.length;
19
+ for (const c of state.active_constraints) {
20
+ if (c.status === 'active' && c.expires_at && c.expires_at < now) {
21
+ c.status = 'expired';
22
+ }
23
+ }
24
+ state.active_constraints = state.active_constraints.filter(c => c.status !== 'expired');
25
+ prunedCount = originalLength - state.active_constraints.length;
26
+ saveState(state, cwd);
27
+ expiredClaimsCount = expireStaleActiveClaims(cwd);
28
+ if (options.expired) {
29
+ const notes = listRuntimeNotes(undefined, cwd);
30
+ for (const note of notes) {
31
+ if (note.expires_at && note.expires_at < now) {
32
+ try {
33
+ if (deleteRuntimeNote(note, cwd)) {
34
+ expiredNotesCount++;
35
+ }
35
36
  }
37
+ catch { /* ignore */ }
36
38
  }
37
- catch { /* ignore */ }
38
39
  }
39
40
  }
40
- }
41
+ writeFileAtomic(memoryPath('project.md', cwd), generateMarkdown(loadState(cwd), cwd));
42
+ });
41
43
  if (options.expired) {
42
44
  console.log(`✔ Pruned ${prunedCount} expired constraints, ${expiredNotesCount} expired runtime notes, ${expiredClaimsCount} expired claims.`);
43
45
  }
@@ -1,9 +1,8 @@
1
1
  import fs from 'node:fs';
2
- import { memoryExists, memoryPath, writeFileAtomic } from '../core/io.js';
2
+ import { memoryExists } from '../core/io.js';
3
3
  import { loadConfig } from '../core/config.js';
4
4
  import { buildOperationalIdentity } from '../core/identity.js';
5
- import { loadState, saveState } from '../core/state.js';
6
- import { generateMarkdown } from '../core/markdown.js';
5
+ import { loadState, persistState } from '../core/state.js';
7
6
  import { scanText } from '../core/security.js';
8
7
  import { nowISO, generateIdWithLabel } from '../core/ids.js';
9
8
  import { saveCandidate, generateCandidateIdWithLabel, listCandidates } from '../core/candidates.js';
@@ -265,8 +264,7 @@ function promoteCandidateToState(candidate, cwd) {
265
264
  break;
266
265
  }
267
266
  }
268
- saveState(state, cwd);
269
- writeFileAtomic(memoryPath('project.md', cwd), generateMarkdown(state));
267
+ persistState(state, cwd);
270
268
  return promotedItemId;
271
269
  }
272
270
  export function mapEventTypeToCandidateType(eventType) {
@@ -1,5 +1,5 @@
1
1
  import { memoryExists } from '../core/io.js';
2
- import { memoryPath, writeFileAtomic } from '../core/io.js';
2
+ import { memoryPath, withStoreLock, writeFileAtomic } from '../core/io.js';
3
3
  import { loadClaim, listClaims, releaseClaim } from '../core/claims.js';
4
4
  import { generateMarkdown } from '../core/markdown.js';
5
5
  import { loadState, saveState } from '../core/state.js';
@@ -9,27 +9,30 @@ export function runReleaseClaim(id, options = {}) {
9
9
  process.exit(1);
10
10
  }
11
11
  try {
12
- const existing = loadClaim(id, options.cwd);
13
- const claim = releaseClaim(id, options.cwd);
14
- let state = loadState(options.cwd);
15
- if (existing.plan_id) {
16
- const plan = state.plan_items.find((item) => item.id === existing.plan_id);
17
- if (plan) {
18
- const otherActiveClaims = listClaims(options.cwd).filter((item) => item.status === 'active' && item.plan_id === existing.plan_id);
19
- if (options.planStatus) {
20
- plan.status = options.planStatus;
12
+ let claim = loadClaim(id, options.cwd);
13
+ withStoreLock(options.cwd, () => {
14
+ const existing = loadClaim(id, options.cwd);
15
+ claim = releaseClaim(id, options.cwd);
16
+ let state = loadState(options.cwd);
17
+ if (existing.plan_id) {
18
+ const plan = state.plan_items.find((item) => item.id === existing.plan_id);
19
+ if (plan) {
20
+ const otherActiveClaims = listClaims(options.cwd).filter((item) => item.status === 'active' && item.plan_id === existing.plan_id);
21
+ if (options.planStatus) {
22
+ plan.status = options.planStatus;
23
+ }
24
+ else if (otherActiveClaims.length === 0 && plan.status === 'in_progress') {
25
+ plan.status = 'todo';
26
+ }
27
+ if (otherActiveClaims.length === 0 && plan.assignee === existing.agent) {
28
+ plan.assignee = undefined;
29
+ }
30
+ plan.updated_at = new Date().toISOString();
31
+ saveState(state, options.cwd);
21
32
  }
22
- else if (otherActiveClaims.length === 0 && plan.status === 'in_progress') {
23
- plan.status = 'todo';
24
- }
25
- if (otherActiveClaims.length === 0 && plan.assignee === existing.agent) {
26
- plan.assignee = undefined;
27
- }
28
- plan.updated_at = new Date().toISOString();
29
- saveState(state, options.cwd);
30
33
  }
31
- }
32
- writeFileAtomic(memoryPath('project.md', options.cwd), generateMarkdown(state, options.cwd));
34
+ writeFileAtomic(memoryPath('project.md', options.cwd), generateMarkdown(state, options.cwd));
35
+ });
33
36
  console.log(`✔ Claim [${id}] released (was: ${claim.agent} → ${claim.scope})`);
34
37
  }
35
38
  catch (e) {
@@ -1,5 +1,5 @@
1
1
  import { execSync } from 'node:child_process';
2
- import { memoryExists, memoryPath, writeFileAtomic } from '../core/io.js';
2
+ import { memoryExists, memoryPath, withStoreLock, writeFileAtomic } from '../core/io.js';
3
3
  import { listClaims, releaseClaim } from '../core/claims.js';
4
4
  import { generateMarkdown } from '../core/markdown.js';
5
5
  import { loadState, saveState } from '../core/state.js';
@@ -35,31 +35,32 @@ export function runReleaseClaims(options = {}) {
35
35
  const toRelease = claims.filter((c) => changedFiles.some((f) => scopeMatchesFile(c.scope, f)));
36
36
  if (toRelease.length === 0)
37
37
  process.exit(0);
38
- let state = loadState(options.cwd);
39
38
  let released = 0;
40
- for (const claim of toRelease) {
41
- try {
42
- releaseClaim(claim.id, options.cwd);
43
- released++;
44
- // Revert in_progress plan to todo if no more active claims
45
- if (claim.plan_id) {
46
- state = loadState(options.cwd);
47
- const plan = state.plan_items.find((p) => p.id === claim.plan_id);
48
- if (plan) {
49
- const remaining = listClaims(options.cwd).filter((c) => c.status === 'active' && c.plan_id === claim.plan_id);
50
- if (remaining.length === 0 && plan.status === 'in_progress') {
51
- plan.status = 'todo';
52
- plan.updated_at = new Date().toISOString();
53
- saveState(state, options.cwd);
39
+ withStoreLock(options.cwd, () => {
40
+ let state = loadState(options.cwd);
41
+ for (const claim of toRelease) {
42
+ try {
43
+ releaseClaim(claim.id, options.cwd);
44
+ released++;
45
+ if (claim.plan_id) {
46
+ state = loadState(options.cwd);
47
+ const plan = state.plan_items.find((p) => p.id === claim.plan_id);
48
+ if (plan) {
49
+ const remaining = listClaims(options.cwd).filter((c) => c.status === 'active' && c.plan_id === claim.plan_id);
50
+ if (remaining.length === 0 && plan.status === 'in_progress') {
51
+ plan.status = 'todo';
52
+ plan.updated_at = new Date().toISOString();
53
+ saveState(state, options.cwd);
54
+ }
54
55
  }
55
56
  }
57
+ console.log(`✔ Auto-released claim [${claim.id}]: ${claim.scope}`);
56
58
  }
57
- console.log(`✔ Auto-released claim [${claim.id}]: ${claim.scope}`);
59
+ catch { /* skip individual failures */ }
58
60
  }
59
- catch { /* skip individual failures */ }
60
- }
61
- state = loadState(options.cwd);
62
- writeFileAtomic(memoryPath('project.md', options.cwd), generateMarkdown(state, options.cwd));
61
+ state = loadState(options.cwd);
62
+ writeFileAtomic(memoryPath('project.md', options.cwd), generateMarkdown(state, options.cwd));
63
+ });
63
64
  if (released > 0) {
64
65
  console.log(`brainclaw: ${released} claim(s) auto-released after merge.`);
65
66
  }
@@ -1,6 +1,6 @@
1
1
  import { memoryExists } from '../core/io.js';
2
2
  import { readAuditLog } from '../core/audit.js';
3
- import { loadState, saveState } from '../core/state.js';
3
+ import { loadState, persistState } from '../core/state.js';
4
4
  import { loadCandidate, saveCandidate } from '../core/candidates.js';
5
5
  import { appendAuditEntry } from '../core/audit.js';
6
6
  import { buildOperationalIdentity } from '../core/identity.js';
@@ -128,7 +128,7 @@ function applyRollback(entry, actorName, options) {
128
128
  state.known_traps.push(before);
129
129
  }
130
130
  }
131
- saveState(state);
131
+ persistState(state);
132
132
  }
133
133
  else {
134
134
  return { ok: false, message: `Rollback not supported for item_type '${item_type}'.` };
@@ -1,7 +1,6 @@
1
- import { loadState, saveState } from '../core/state.js';
1
+ import { loadState, persistState } from '../core/state.js';
2
2
  import { resolveCurrentAgentName } from '../core/agent-registry.js';
3
- import { memoryExists, memoryPath, writeFileAtomic } from '../core/io.js';
4
- import { generateMarkdown } from '../core/markdown.js';
3
+ import { memoryExists } from '../core/io.js';
5
4
  import { generateIdWithLabel, nowISO } from '../core/ids.js';
6
5
  import { loadConfig } from '../core/config.js';
7
6
  import { scanText } from '../core/security.js';
@@ -89,8 +88,7 @@ function runToolAdd(name, description, options, cwd) {
89
88
  // For now, store as decision to avoid schema migration
90
89
  // Will migrate to separate tool storage in v0.16
91
90
  state.recent_decisions.push(entry);
92
- saveState(state, cwd);
93
- writeFileAtomic(memoryPath('project.md', cwd), generateMarkdown(state));
91
+ persistState(state, cwd);
94
92
  console.log(`✔ Tool added: [${id}] ${name}`);
95
93
  console.log(' (Stored in decisions for now; will move to dedicated registry in v0.16)');
96
94
  }
@@ -1,11 +1,10 @@
1
- import { loadState, saveState } from '../core/state.js';
1
+ import { loadState, persistState } from '../core/state.js';
2
2
  import { resolveCurrentAgentName } from '../core/agent-registry.js';
3
3
  import { resolveCurrentHostId } from '../core/host.js';
4
4
  import { loadConfig } from '../core/config.js';
5
- import { generateMarkdown } from '../core/markdown.js';
6
5
  import { nowISO } from '../core/ids.js';
7
6
  import { scanText } from '../core/security.js';
8
- import { memoryExists, memoryPath, writeFileAtomic } from '../core/io.js';
7
+ import { memoryExists } from '../core/io.js';
9
8
  import { generateTrapIdWithLabel, saveOperationalTrap } from '../core/traps.js';
10
9
  import { validateCliInput, validateCliTtl } from '../core/input-validation.js';
11
10
  import { resolveTargetStore } from '../core/store-resolution.js';
@@ -49,8 +48,7 @@ export function runTrap(text, options = {}) {
49
48
  };
50
49
  if (visibility === 'shared') {
51
50
  state.known_traps.push(entry);
52
- saveState(state, cwd);
53
- writeFileAtomic(memoryPath('project.md', cwd), generateMarkdown(state, cwd));
51
+ persistState(state, cwd);
54
52
  }
55
53
  else {
56
54
  saveOperationalTrap(entry, cwd);
@@ -1,6 +1,5 @@
1
- import { loadState, saveState } from '../core/state.js';
2
- import { memoryExists, memoryPath, writeFileAtomic } from '../core/io.js';
3
- import { generateMarkdown } from '../core/markdown.js';
1
+ import { loadState, persistState } from '../core/state.js';
2
+ import { memoryExists } from '../core/io.js';
4
3
  export function runUpdateHandoff(id, options = {}) {
5
4
  if (!memoryExists()) {
6
5
  console.error('Error: .brainclaw/ not found. Run `brainclaw init` first.');
@@ -16,8 +15,7 @@ export function runUpdateHandoff(id, options = {}) {
16
15
  handoff.status = options.status;
17
16
  if (options.to !== undefined)
18
17
  handoff.to = options.to;
19
- saveState(state);
20
- writeFileAtomic(memoryPath('project.md'), generateMarkdown(state));
18
+ persistState(state);
21
19
  console.log(`✔ Handoff updated: [${handoff.id}] ${handoff.from} → ${handoff.to} (${handoff.status})`);
22
20
  }
23
21
  //# sourceMappingURL=update-handoff.js.map
@@ -1,6 +1,5 @@
1
- import { loadState, saveState } from '../core/state.js';
2
- import { memoryExists, memoryPath, writeFileAtomic } from '../core/io.js';
3
- import { generateMarkdown } from '../core/markdown.js';
1
+ import { loadState, persistState } from '../core/state.js';
2
+ import { memoryExists } from '../core/io.js';
4
3
  import { nowISO } from '../core/ids.js';
5
4
  export function runUpdatePlan(id, options = {}) {
6
5
  if (!memoryExists(options.cwd)) {
@@ -30,8 +29,7 @@ export function runUpdatePlan(id, options = {}) {
30
29
  if (options.actualEffort)
31
30
  plan.actual_effort = options.actualEffort;
32
31
  plan.updated_at = timestamp;
33
- saveState(state, options.cwd);
34
- writeFileAtomic(memoryPath('project.md', options.cwd), generateMarkdown(state, options.cwd));
32
+ persistState(state, options.cwd);
35
33
  console.log(`✔ Plan item updated: [${plan.id}] ${plan.text}`);
36
34
  }
37
35
  //# sourceMappingURL=update-plan.js.map
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { ensureMemoryDir, memoryDir, memoryExists } from '../core/io.js';
4
- import { loadState, saveState } from '../core/state.js';
4
+ import { loadState, persistState } from '../core/state.js';
5
5
  import { scanMigrationStatus } from '../core/migration.js';
6
6
  import { commitMemoryChange, initMemoryRepo } from '../core/memory-git.js';
7
7
  import { BRAINCLAW_SECTION_END, BRAINCLAW_SECTION_START, buildBrainclawSection, buildClaudeCodeCommandText, ensureClaudeCodeCommand, hasBrainclawSection, } from '../core/agent-files.js';
@@ -114,7 +114,7 @@ export function runUpgrade(options = {}) {
114
114
  // Execute schema migrations by re-saving state (loadState auto-migrates via Zod parse)
115
115
  if (outdated.length > 0) {
116
116
  const state = loadState(cwd);
117
- saveState(state, cwd);
117
+ persistState(state, cwd);
118
118
  }
119
119
  const refreshedAgentFiles = refreshManagedWorkspaceAgentFiles(cwd);
120
120
  // Clean up empty legacy directories (recursively removes empty subdirs first)
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { memoryDir } from './io.js';
3
+ import { memoryDir, withStoreLock } from './io.js';
4
4
  import { nowISO } from './ids.js';
5
5
  import { logger } from './logger.js';
6
6
  import { appendEvent } from './event-log.js';
@@ -23,31 +23,31 @@ function auditLogPath(cwd) {
23
23
  }
24
24
  export function appendAuditEntry(entry, cwd) {
25
25
  try {
26
- const full = {
27
- timestamp: nowISO(),
28
- actor: entry.actor,
29
- action: entry.action,
30
- item_id: entry.item_id,
31
- item_type: entry.item_type,
32
- before: entry.before,
33
- after: entry.after,
34
- actor_id: entry.actor_id,
35
- reason: entry.reason,
36
- };
37
- // Remove undefined fields for compactness
38
- const line = JSON.stringify(Object.fromEntries(Object.entries(full).filter(([, v]) => v !== undefined)));
39
- fs.appendFileSync(auditLogPath(cwd), line + '\n', 'utf-8');
40
- // Mirror to structured event log
41
- const eventAction = AUDIT_TO_EVENT_ACTION[entry.action];
42
- if (eventAction) {
43
- appendEvent({
44
- action: eventAction,
45
- item_type: entry.item_type ?? 'state',
26
+ withStoreLock(cwd, () => {
27
+ const full = {
28
+ timestamp: nowISO(),
29
+ actor: entry.actor,
30
+ action: entry.action,
46
31
  item_id: entry.item_id,
47
- agent: entry.actor,
48
- agent_id: entry.actor_id,
49
- }, cwd);
50
- }
32
+ item_type: entry.item_type,
33
+ before: entry.before,
34
+ after: entry.after,
35
+ actor_id: entry.actor_id,
36
+ reason: entry.reason,
37
+ };
38
+ const line = JSON.stringify(Object.fromEntries(Object.entries(full).filter(([, v]) => v !== undefined)));
39
+ fs.appendFileSync(auditLogPath(cwd), line + '\n', 'utf-8');
40
+ const eventAction = AUDIT_TO_EVENT_ACTION[entry.action];
41
+ if (eventAction) {
42
+ appendEvent({
43
+ action: eventAction,
44
+ item_type: entry.item_type ?? 'state',
45
+ item_id: entry.item_id,
46
+ agent: entry.actor,
47
+ agent_id: entry.actor_id,
48
+ }, cwd);
49
+ }
50
+ });
51
51
  }
52
52
  catch (err) {
53
53
  logger.debug('Failed to write audit log entry:', err);