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
@@ -2,7 +2,7 @@ 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 } from './io.js';
5
+ import { resolveEntityDir, withStoreLock } from './io.js';
6
6
  import { nowISO, getNextShortLabel } from './ids.js';
7
7
  import { JsonStore } from './json-store.js';
8
8
  function inboxDir(cwd, mode = 'read') {
@@ -35,8 +35,10 @@ function candidateStore(dest = 'pending', cwd) {
35
35
  });
36
36
  }
37
37
  export function saveCandidate(candidate, cwd) {
38
- ensureInboxDirs(cwd);
39
- candidateStore('pending', cwd).save(CandidateSchema.parse(candidate));
38
+ withStoreLock(cwd, () => {
39
+ ensureInboxDirs(cwd);
40
+ candidateStore('pending', cwd).save(CandidateSchema.parse(candidate));
41
+ });
40
42
  }
41
43
  export function loadCandidate(id, cwd) {
42
44
  return candidateStore('pending', cwd).load(id);
@@ -49,9 +51,11 @@ export function listCandidates(status, cwd) {
49
51
  return status ? candidates.filter((candidate) => candidate.status === status) : candidates;
50
52
  }
51
53
  export function archiveCandidate(candidate, dest, cwd) {
52
- ensureInboxDirs(cwd);
53
- candidateStore(dest, cwd).save(CandidateSchema.parse(candidate));
54
- candidateStore('pending', cwd).delete(candidate.id);
54
+ withStoreLock(cwd, () => {
55
+ ensureInboxDirs(cwd);
56
+ candidateStore(dest, cwd).save(CandidateSchema.parse(candidate));
57
+ candidateStore('pending', cwd).delete(candidate.id);
58
+ });
55
59
  }
56
60
  export function listArchivedCandidates(dest, cwd) {
57
61
  return candidateStore(dest, cwd).list();
@@ -1,7 +1,7 @@
1
1
  import crypto from 'node:crypto';
2
2
  import fs from 'node:fs';
3
3
  import { ClaimSchema } from './schema.js';
4
- import { resolveEntityDir } from './io.js';
4
+ import { resolveEntityDir, withStoreLock } from './io.js';
5
5
  import { nowISO } from './ids.js';
6
6
  import { JsonStore } from './json-store.js';
7
7
  function claimsDir(cwd, mode = 'read') {
@@ -22,14 +22,16 @@ function claimStore(cwd) {
22
22
  });
23
23
  }
24
24
  export function saveClaim(claim, cwd) {
25
- ensureClaimsDir(cwd);
26
- const writeStore = new JsonStore({
27
- dirPath: claimsDir(cwd, 'write'),
28
- documentType: 'claim',
29
- getId: (c) => c.id,
30
- sort: (a, b) => a.created_at.localeCompare(b.created_at),
25
+ withStoreLock(cwd, () => {
26
+ ensureClaimsDir(cwd);
27
+ const writeStore = new JsonStore({
28
+ dirPath: claimsDir(cwd, 'write'),
29
+ documentType: 'claim',
30
+ getId: (c) => c.id,
31
+ sort: (a, b) => a.created_at.localeCompare(b.created_at),
32
+ });
33
+ writeStore.save(ClaimSchema.parse(claim));
31
34
  });
32
- writeStore.save(ClaimSchema.parse(claim));
33
35
  }
34
36
  export function loadClaim(id, cwd) {
35
37
  return claimStore(cwd).load(id);
@@ -5,7 +5,7 @@ import { listClaims } from './claims.js';
5
5
  import { inferProjectFromTarget, loadInstructions, resolveInstructions } from './instructions.js';
6
6
  import { buildReputationSummary, findAgentReputationSummary } from './reputation.js';
7
7
  import { listRuntimeNotes } from './runtime.js';
8
- import { loadState, saveState } from './state.js';
8
+ import { loadState, persistState } from './state.js';
9
9
  export function buildCoordinationSnapshot(options = {}) {
10
10
  const config = loadConfig(options.cwd);
11
11
  const state = loadState(options.cwd);
@@ -58,7 +58,7 @@ export function buildCoordinationSnapshot(options = {}) {
58
58
  }
59
59
  }
60
60
  if (changed)
61
- saveState(state, options.cwd);
61
+ persistState(state, options.cwd);
62
62
  }
63
63
  return {
64
64
  project_id: config.project_id,
@@ -1,5 +1,5 @@
1
1
  import fs from 'node:fs';
2
- import { resolveEntityDir } from './io.js';
2
+ import { resolveEntityDir, withStoreLock } from './io.js';
3
3
  import { generateId } from './ids.js';
4
4
  import { InstructionEntrySchema } from './schema.js';
5
5
  import { JsonStore } from './json-store.js';
@@ -19,7 +19,9 @@ export function loadInstructions(cwd) {
19
19
  return instructionStore(cwd).list();
20
20
  }
21
21
  export function saveInstruction(entry, cwd) {
22
- instructionStore(cwd, 'write').save(InstructionEntrySchema.parse(entry));
22
+ withStoreLock(cwd, () => {
23
+ instructionStore(cwd, 'write').save(InstructionEntrySchema.parse(entry));
24
+ });
23
25
  }
24
26
  export function createInstruction(text, options, cwd) {
25
27
  const entries = loadInstructions(cwd);
package/dist/core/io.js CHANGED
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { withLock, cleanStaleLocks } from './lock.js';
4
4
  export const MEMORY_DIR = '.brainclaw';
5
+ const STORE_LOCK_BASENAME = '.store-mutation';
5
6
  const RETRYABLE_RENAME_ERROR_CODES = new Set(['EPERM', 'EBUSY', 'EACCES']);
6
7
  const DEFAULT_RENAME_RETRY_ATTEMPTS = 6;
7
8
  const DEFAULT_RENAME_RETRY_DELAY_MS = 25;
@@ -72,6 +73,9 @@ export function memoryDir(cwd = process.cwd(), preferredDirName) {
72
73
  export function memoryPath(filename, cwd, preferredDirName) {
73
74
  return path.join(memoryDir(cwd, preferredDirName), filename);
74
75
  }
76
+ export function storeLockPath(cwd, preferredDirName) {
77
+ return memoryPath(STORE_LOCK_BASENAME, cwd, preferredDirName);
78
+ }
75
79
  export function memoryExists(cwd, preferredDirName) {
76
80
  return fs.existsSync(memoryDir(cwd, preferredDirName));
77
81
  }
@@ -95,6 +99,10 @@ export function ensureMemoryDir(cwd, preferredDirName) {
95
99
  }
96
100
  }
97
101
  }
102
+ export function withStoreLock(cwd = process.cwd(), fn, preferredDirName) {
103
+ ensureMemoryDir(cwd, preferredDirName);
104
+ return withLock(storeLockPath(cwd, preferredDirName), fn);
105
+ }
98
106
  /** Check if a path is a file, or a directory with at least one entry. */
99
107
  function hasContent(p) {
100
108
  try {
package/dist/core/lock.js CHANGED
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  const DEFAULT_TIMEOUT_MS = 2000;
4
4
  const LOCK_RETRY_INTERVAL_MS = 50;
5
5
  const LOCK_EXPIRY_MS = 5000;
6
+ const heldLocks = new Map();
6
7
  function lockFilePath(targetPath) {
7
8
  return targetPath + '.lock';
8
9
  }
@@ -59,22 +60,37 @@ function tryBreakLock(lockPath) {
59
60
  }
60
61
  export function acquireLock(targetPath, timeoutMs = DEFAULT_TIMEOUT_MS) {
61
62
  const lockPath = lockFilePath(targetPath);
63
+ const heldCount = heldLocks.get(lockPath);
64
+ if (heldCount) {
65
+ heldLocks.set(lockPath, heldCount + 1);
66
+ return true;
67
+ }
62
68
  const deadline = Date.now() + timeoutMs;
63
69
  const dir = path.dirname(lockPath);
64
70
  if (!fs.existsSync(dir)) {
65
71
  fs.mkdirSync(dir, { recursive: true });
66
72
  }
67
73
  while (Date.now() < deadline) {
68
- if (tryCreateLock(lockPath))
74
+ if (tryCreateLock(lockPath)) {
75
+ heldLocks.set(lockPath, 1);
69
76
  return true;
70
- if (tryBreakLock(lockPath))
77
+ }
78
+ if (tryBreakLock(lockPath)) {
79
+ heldLocks.set(lockPath, 1);
71
80
  return true;
81
+ }
72
82
  syncSleep(Math.min(LOCK_RETRY_INTERVAL_MS, deadline - Date.now()));
73
83
  }
74
84
  return false;
75
85
  }
76
86
  export function releaseLock(targetPath) {
77
87
  const lockPath = lockFilePath(targetPath);
88
+ const heldCount = heldLocks.get(lockPath);
89
+ if (heldCount && heldCount > 1) {
90
+ heldLocks.set(lockPath, heldCount - 1);
91
+ return;
92
+ }
93
+ heldLocks.delete(lockPath);
78
94
  try {
79
95
  if (fs.existsSync(lockPath)) {
80
96
  fs.unlinkSync(lockPath);
@@ -2,7 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import YAML from 'yaml';
4
4
  import { memoryDir, memoryPath, readFileSync, writeFileAtomic, resolveEntityDir } from './io.js';
5
- import { AgentIdentityDocumentSchema, BootstrapProfileDocumentSchema, CandidateSchema, ClaimSchema, ConfigSchema, MemorySeedDocumentSchema, ConstraintSchema, CurrentSessionStateSchema, DecisionSchema, HandoffSchema, InstructionEntrySchema, PlanItemSchema, ProjectIdentityDocumentSchema, RuntimeNoteSchema, SessionSnapshotSchema, TrapSchema, } from './schema.js';
5
+ import { BootstrapApplicationReceiptSchema, BootstrapImportPlanDocumentSchema, AgentIdentityDocumentSchema, BootstrapProfileDocumentSchema, CandidateSchema, ClaimSchema, ConfigSchema, MemorySeedDocumentSchema, ConstraintSchema, CurrentSessionStateSchema, DecisionSchema, HandoffSchema, InstructionEntrySchema, PlanItemSchema, ProjectIdentityDocumentSchema, RuntimeNoteSchema, SessionSnapshotSchema, TrapSchema, } from './schema.js';
6
6
  export class MigrationError extends Error {
7
7
  kind;
8
8
  documentType;
@@ -16,6 +16,8 @@ export class MigrationError extends Error {
16
16
  const CURRENT_SCHEMA_VERSION = 2;
17
17
  const registry = {
18
18
  agent_identity: createRegistryEntry(AgentIdentityDocumentSchema),
19
+ bootstrap_application: createRegistryEntry(BootstrapApplicationReceiptSchema),
20
+ bootstrap_import_plan: createRegistryEntry(BootstrapImportPlanDocumentSchema),
19
21
  bootstrap_profile: createRegistryEntry(BootstrapProfileDocumentSchema),
20
22
  candidate: createRegistryEntry(CandidateSchema),
21
23
  claim: createRegistryEntry(ClaimSchema),
@@ -145,6 +147,8 @@ export function scanMigrationStatus(cwd) {
145
147
  collectSingle(entries, memoryPath('project.identity.json', cwd), 'project_identity');
146
148
  collectSingle(entries, memoryPath('.current-session', cwd), 'current_session');
147
149
  collectSingle(entries, memoryPath(path.join('bootstrap', 'profile.json'), cwd), 'bootstrap_profile');
150
+ collectSingle(entries, memoryPath(path.join('bootstrap', 'import-plan.json'), cwd), 'bootstrap_import_plan');
151
+ collectSingle(entries, memoryPath(path.join('bootstrap', 'last-application.json'), cwd), 'bootstrap_application');
148
152
  const effectiveCwd = cwd ?? process.cwd();
149
153
  collectDirectory(entries, resolveEntityDir('constraints', effectiveCwd, 'read'), 'constraint');
150
154
  collectDirectory(entries, resolveEntityDir('decisions', effectiveCwd, 'read'), 'decision');
@@ -161,7 +165,7 @@ export function scanMigrationStatus(cwd) {
161
165
  collectDirectory(entries, resolveEntityDir('runtime-hosts', effectiveCwd, 'read'), 'runtime_note', true);
162
166
  collectDirectory(entries, resolveEntityDir('runtime-private', effectiveCwd, 'read'), 'runtime_note', true);
163
167
  collectDirectory(entries, resolveEntityDir('instructions', effectiveCwd, 'read'), 'instruction');
164
- collectDirectory(entries, resolveEntityDir('bootstrap', effectiveCwd, 'read'), 'memory_seed');
168
+ collectDirectory(entries, path.join(resolveEntityDir('bootstrap', effectiveCwd, 'read'), 'seeds'), 'memory_seed');
165
169
  collectDirectory(entries, resolveEntityDir('agents', effectiveCwd, 'read'), 'agent_identity');
166
170
  collectDirectory(entries, resolveEntityDir('sessions', effectiveCwd, 'read'), 'session_snapshot');
167
171
  return entries.sort((a, b) => a.path.localeCompare(b.path));
@@ -2,7 +2,7 @@ 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 } from './io.js';
5
+ import { resolveEntityDir, withStoreLock } from './io.js';
6
6
  import { loadVersionedJsonFile, saveVersionedJsonFile } from './migration.js';
7
7
  import { RuntimeNoteSchema } from './schema.js';
8
8
  import { commitMemoryChange } from './memory-git.js';
@@ -40,13 +40,15 @@ export function saveRuntimeNote(note, cwd) {
40
40
  const persistedNote = visibility === 'shared'
41
41
  ? { ...note, visibility, host_id: hostId }
42
42
  : { ...note, visibility, host_id: hostId };
43
- ensureRuntimeDir(note.agent, cwd, visibility, hostId);
44
- const filepath = visibility === 'shared'
45
- ? path.join(sharedAgentDir(note.agent, cwd, 'write'), `${note.id}.json`)
46
- : path.join(hostAgentDir(visibility, hostId, note.agent, cwd, 'write'), `${note.id}.json`);
47
- saveVersionedJsonFile('runtime_note', filepath, RuntimeNoteSchema.parse(persistedNote));
48
- appendEvent({ action: 'create', item_type: 'runtime_note', item_id: note.id, agent: note.agent, agent_id: note.agent_id }, cwd);
49
- commitMemoryChange(`runtime note: ${note.note_type ?? 'note'} (${note.agent})`, cwd);
43
+ withStoreLock(cwd, () => {
44
+ ensureRuntimeDir(note.agent, cwd, visibility, hostId);
45
+ const filepath = visibility === 'shared'
46
+ ? path.join(sharedAgentDir(note.agent, cwd, 'write'), `${note.id}.json`)
47
+ : path.join(hostAgentDir(visibility, hostId, note.agent, cwd, 'write'), `${note.id}.json`);
48
+ saveVersionedJsonFile('runtime_note', filepath, RuntimeNoteSchema.parse(persistedNote));
49
+ appendEvent({ action: 'create', item_type: 'runtime_note', item_id: note.id, agent: note.agent, agent_id: note.agent_id }, cwd);
50
+ commitMemoryChange(`runtime note: ${note.note_type ?? 'note'} (${note.agent})`, cwd);
51
+ });
50
52
  }
51
53
  export function runtimeNotePath(note, cwd) {
52
54
  const visibility = note.visibility ?? 'shared';
@@ -56,12 +58,14 @@ export function runtimeNotePath(note, cwd) {
56
58
  : path.join(hostAgentDir(visibility, hostId, note.agent, cwd), `${note.id}.json`);
57
59
  }
58
60
  export function deleteRuntimeNote(note, cwd) {
59
- const filepath = runtimeNotePath(note, cwd);
60
- if (!fs.existsSync(filepath)) {
61
- return false;
62
- }
63
- fs.unlinkSync(filepath);
64
- return true;
61
+ return withStoreLock(cwd, () => {
62
+ const filepath = runtimeNotePath(note, cwd);
63
+ if (!fs.existsSync(filepath)) {
64
+ return false;
65
+ }
66
+ fs.unlinkSync(filepath);
67
+ return true;
68
+ });
65
69
  }
66
70
  function readAgentNotes(dir, agent) {
67
71
  if (!fs.existsSync(dir))
@@ -451,6 +451,7 @@ export const MemorySeedKindSchema = z.enum([
451
451
  export const MemorySeedSourceKindSchema = z.enum([
452
452
  'readme',
453
453
  'agents_md',
454
+ 'native_instruction',
454
455
  'manifest',
455
456
  'repo_analysis',
456
457
  'git',
@@ -483,6 +484,74 @@ export const BootstrapProfileDocumentSchema = z.object({
483
484
  agents_md_present: z.boolean().default(false),
484
485
  seed_count: z.number().int().nonnegative(),
485
486
  target: z.string().optional(),
487
+ workspace_kind: z.enum(['empty', 'existing']).optional(),
488
+ confidence: MemorySeedConfidenceSchema.optional(),
489
+ native_instruction_files: z.array(z.string()).default([]),
490
+ gaps: z.array(z.string()).default([]),
491
+ });
492
+ export const BootstrapSuggestionTargetSchema = z.enum(['instruction', 'decision', 'constraint', 'trap']);
493
+ export const BootstrapSuggestionDocumentSchema = z.object({
494
+ schema_version: z.number().int().positive().optional(),
495
+ id: z.string(),
496
+ target: BootstrapSuggestionTargetSchema,
497
+ text: z.string(),
498
+ rationale: z.string(),
499
+ confidence: MemorySeedConfidenceSchema,
500
+ source_seed_ids: z.array(z.string()).default([]),
501
+ source_refs: z.array(z.string()).default([]),
502
+ layer: z.enum(['global', 'project', 'agent']).optional(),
503
+ scope: z.string().optional(),
504
+ tags: z.array(z.string()).default([]),
505
+ related_paths: z.array(z.string()).optional(),
506
+ reversible: z.boolean().default(true),
507
+ });
508
+ export const BootstrapInterviewAudienceSchema = z.enum(['cli', 'ide_chat', 'any']);
509
+ export const BootstrapInterviewQuestionSchema = z.object({
510
+ id: z.string(),
511
+ prompt: z.string(),
512
+ rationale: z.string(),
513
+ priority: z.enum(['high', 'medium', 'low']),
514
+ audience: BootstrapInterviewAudienceSchema.default('any'),
515
+ response_kind: z.enum(['short_text', 'long_text', 'boolean', 'list']).default('short_text'),
516
+ gap_keys: z.array(z.string()).default([]),
517
+ });
518
+ export const BootstrapInterviewPlanSchema = z.object({
519
+ schema_version: z.number().int().positive().optional(),
520
+ derived_at: z.string(),
521
+ workspace_kind: z.enum(['empty', 'existing']).optional(),
522
+ audience: BootstrapInterviewAudienceSchema.default('any'),
523
+ summary: z.string(),
524
+ question_count: z.number().int().nonnegative(),
525
+ questions: z.array(BootstrapInterviewQuestionSchema).default([]),
526
+ });
527
+ export const BootstrapImportPlanDocumentSchema = z.object({
528
+ schema_version: z.number().int().positive().optional(),
529
+ derived_at: z.string(),
530
+ target: z.string().optional(),
531
+ workspace_kind: z.enum(['empty', 'existing']).optional(),
532
+ confidence: MemorySeedConfidenceSchema.optional(),
533
+ summary: z.string(),
534
+ requires_confirmation: z.boolean().default(true),
535
+ gaps: z.array(z.string()).default([]),
536
+ suggestion_count: z.number().int().nonnegative(),
537
+ suggestions: z.array(BootstrapSuggestionDocumentSchema).default([]),
538
+ interview: BootstrapInterviewPlanSchema.optional(),
539
+ });
540
+ export const BootstrapManagedArtifactSchema = z.object({
541
+ kind: z.enum(['instruction', 'decision', 'constraint', 'trap']),
542
+ id: z.string(),
543
+ suggestion_id: z.string(),
544
+ rollback_action: z.enum(['deactivate', 'delete']).default('delete'),
545
+ });
546
+ export const BootstrapApplicationReceiptSchema = z.object({
547
+ schema_version: z.number().int().positive().optional(),
548
+ applied_at: z.string(),
549
+ proposal_derived_at: z.string(),
550
+ target: z.string().optional(),
551
+ workspace_kind: z.enum(['empty', 'existing']).optional(),
552
+ managed_artifacts: z.array(BootstrapManagedArtifactSchema).default([]),
553
+ suggestion_ids: z.array(z.string()).default([]),
554
+ uninstalled_at: z.string().optional(),
486
555
  });
487
556
  export const AgentIntegrationNameSchema = z.enum([
488
557
  'github-copilot',
@@ -1,10 +1,11 @@
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, resolveEntityDir } from './io.js';
4
+ import { ensureMemoryDir, memoryPath, resolveEntityDir, withStoreLock, writeFileAtomic } from './io.js';
5
5
  import { commitMemoryChange } from './memory-git.js';
6
6
  import { appendEvent } from './event-log.js';
7
7
  import { loadVersionedJsonFile, saveVersionedJsonFile } from './migration.js';
8
+ import { generateMarkdown } from './markdown.js';
8
9
  export function emptyState() {
9
10
  return {
10
11
  version: 1,
@@ -69,15 +70,31 @@ function syncDirectory(dirPath, items, documentType) {
69
70
  }
70
71
  }
71
72
  export function saveState(state, cwd) {
73
+ persistState(state, cwd, { writeProjectMarkdown: false });
74
+ }
75
+ function writeStateDirectories(state, cwd) {
72
76
  ensureMemoryDir(cwd);
73
77
  const effectiveCwd = cwd ?? process.cwd();
74
- // Write to entity-aligned directories
75
78
  syncDirectory(resolveEntityDir('constraints', effectiveCwd, 'write'), state.active_constraints, 'constraint');
76
79
  syncDirectory(resolveEntityDir('decisions', effectiveCwd, 'write'), state.recent_decisions, 'decision');
77
80
  syncDirectory(resolveEntityDir('traps', effectiveCwd, 'write'), state.known_traps, 'trap');
78
81
  syncDirectory(resolveEntityDir('handoffs', effectiveCwd, 'write'), state.open_handoffs, 'handoff');
79
82
  syncDirectory(resolveEntityDir('plans', effectiveCwd, 'write'), state.plan_items, 'plan');
80
- appendEvent({ action: 'update', item_type: 'state', agent: 'system' }, cwd);
81
- commitMemoryChange('state update', cwd);
83
+ }
84
+ export function persistState(state, cwd, options = {}) {
85
+ const effectiveCwd = cwd ?? process.cwd();
86
+ withStoreLock(effectiveCwd, () => {
87
+ writeStateDirectories(state, effectiveCwd);
88
+ if (options.writeProjectMarkdown ?? true) {
89
+ writeFileAtomic(memoryPath('project.md', effectiveCwd), generateMarkdown(state, effectiveCwd));
90
+ }
91
+ appendEvent({
92
+ action: options.eventAction ?? 'update',
93
+ item_type: 'state',
94
+ agent: 'system',
95
+ summary: options.eventSummary,
96
+ }, effectiveCwd);
97
+ commitMemoryChange(options.commitMessage ?? 'state update', effectiveCwd);
98
+ });
82
99
  }
83
100
  //# sourceMappingURL=state.js.map
package/docs/cli.md CHANGED
@@ -1,6 +1,15 @@
1
1
  # CLI Reference
2
2
 
3
- Commands grouped by purpose. All commands are available as `brainclaw` or its alias `bclaw`.
3
+ The CLI is Brainclaw's explicit operator, scripting, release, and fallback surface. All commands are available as `brainclaw` or its alias `bclaw`.
4
+
5
+ For capable coding agents, prefer MCP for dynamic runtime state:
6
+
7
+ - `bclaw_bootstrap` instead of manual bootstrap polling
8
+ - `bclaw_get_context` instead of repeated raw CLI context calls
9
+ - `bclaw_list_plans` / `bclaw_list_claims` / `bclaw_get_agent_board` for live coordination views
10
+ - `bclaw_claim`, `bclaw_write_note`, and `bclaw_session_end` for session continuity
11
+
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.
4
13
 
5
14
  ---
6
15
 
@@ -97,15 +106,24 @@ brainclaw rebuild
97
106
 
98
107
  Bootstrap shared memory for a new agent or project context.
99
108
 
109
+ For capable agents, the MCP equivalent is `bclaw_bootstrap`. Use the CLI when a human is driving onboarding, reviewing the import proposal, or operating in fallback mode.
110
+
100
111
  | Option | Description |
101
112
  |---|---|
102
113
  | `--for <agent>` | Target agent name |
103
114
  | `--json` | Output as JSON |
104
115
  | `--refresh` | Force refresh even if bootstrap data is recent |
116
+ | `--interview` | Render adaptive interview prompts instead of the bootstrap summary |
117
+ | `--audience <audience>` | Filter interview prompts for `cli`, `ide_chat`, or `any` |
118
+ | `--apply` | Import the current bootstrap proposal into canonical memory |
119
+ | `--uninstall` | Deactivate the last bootstrap-managed import |
120
+ | `-y, --yes` | Skip confirmation prompts for apply/uninstall |
105
121
 
106
122
  ```bash
107
123
  brainclaw bootstrap --for copilot
108
124
  brainclaw bootstrap --for claude --refresh --json
125
+ brainclaw bootstrap --interview --audience cli
126
+ brainclaw bootstrap --apply -y
109
127
  ```
110
128
 
111
129
  ### `brainclaw env`
@@ -574,6 +592,8 @@ brainclaw search "rollout" --since 2026-01-01 --json
574
592
 
575
593
  ## Planning and Coordination
576
594
 
595
+ For capable agents, prefer the MCP plan and claim tools for live runtime interactions. The CLI remains the operator and scripting form of the same coordination model.
596
+
577
597
  ### `brainclaw plan create <text>`
578
598
 
579
599
  Create a shared work item.
@@ -10,30 +10,43 @@ That means brainclaw integration should not rely on only one mechanism such as:
10
10
  - a single skill
11
11
  - a single startup command
12
12
 
13
- ## Better approach
14
-
15
- Use multiple points of contact:
16
-
17
- - lightweight system instructions
18
- - project-specific context retrieval
19
- - prompt-ready generated context
20
- - MCP tools when available
21
- - board and status views
22
- - workflow reminders around plans, claims, and handoffs
13
+ ## Better approach
14
+
15
+ Use multiple points of contact:
16
+
17
+ - lightweight system instructions
18
+ - project-specific context retrieval through MCP when available
19
+ - prompt-ready generated context
20
+ - native agent files for local reminders
21
+ - MCP tools for dynamic state
22
+ - board and status views
23
+ - workflow reminders around plans, claims, and handoffs
24
+
25
+ ## Surface hierarchy
26
+
27
+ The default order is:
28
+
29
+ 1. MCP for live state
30
+ 2. native agent files for local workflow guidance
31
+ 3. readable files as fallback
32
+ 4. CLI for setup, operator tasks, scripting, and fallback workflows
33
+
34
+ This keeps dynamic state dynamic instead of trying to encode it permanently into static instructions.
23
35
 
24
36
  ## System instructions vs project instructions
25
37
 
26
38
  Keep these separate.
27
39
 
28
- ### System instructions
29
- How the agent should use brainclaw.
40
+ ### System instructions
41
+ How the agent should use brainclaw.
30
42
 
31
43
  Examples:
32
44
 
33
- - check workspace memory before significant changes
34
- - bootstrap if the workspace is not initialized and the workflow allows it
35
- - respect file claims
36
- - update shared plan status when appropriate
45
+ - check workspace memory before significant changes
46
+ - prefer MCP over manual CLI calls when MCP is available
47
+ - bootstrap if the workspace is not initialized and the workflow allows it
48
+ - respect file claims
49
+ - update shared plan status when appropriate
37
50
 
38
51
  ### Project instructions
39
52
  What is true for the current workspace.
@@ -86,9 +99,9 @@ By default, `--write` treats generated workspace files as local setup and adds t
86
99
 
87
100
  If a repo already contains tracked local agent files from an older setup, `brainclaw session-start` warns at the beginning of work and `brainclaw doctor --fix-agent-ignore` can repair the missing `.gitignore` entries. Tracked files still need to be untracked manually with Git after the ignore rules are in place.
88
101
 
89
- ## MCP server workflow
90
-
91
- The brainclaw MCP server exposes all memory operations as tools that agents can call directly.
102
+ ## MCP server workflow
103
+
104
+ The brainclaw MCP server exposes the dynamic Brainclaw workflow directly. For capable agents, this is the nominal runtime path.
92
105
 
93
106
  ### Starting the MCP server
94
107
 
@@ -116,19 +129,19 @@ Most agents pick this up via their MCP config file (`.mcp.json`, `~/.cursor/mcp.
116
129
  | `bclaw_bootstrap` | Initialize the workspace if not already done |
117
130
  | `bclaw_get_execution_context` | Get execution context (identity, claims, active plans) |
118
131
 
119
- ## Session lifecycle
120
-
121
- ### Starting a session
122
-
123
- ```bash
124
- brainclaw session-start --agent my-agent --model claude-opus-4-5
132
+ ## Session lifecycle
133
+
134
+ ### Starting a session
135
+
136
+ ```bash
137
+ brainclaw session-start --agent my-agent --model claude-opus-4-5
125
138
  ```
126
139
 
127
140
  This registers the agent's identity and optionally records the model being used. Other agents can see active sessions in `list-claims` and `board`.
128
141
 
129
- ### During the session
130
-
131
- At each significant step:
142
+ ### During the session
143
+
144
+ For MCP-capable agents, prefer the MCP equivalents of these actions. The CLI examples below are the operator and fallback form of the same lifecycle.
132
145
 
133
146
  ```bash
134
147
  brainclaw context --json # load fresh project state
@@ -146,7 +159,7 @@ brainclaw session-end --auto-release
146
159
 
147
160
  This releases all active claims held by the current agent and updates plan statuses.
148
161
 
149
- ## Generated files are local-only
162
+ ## Generated files are local-only
150
163
 
151
164
  Agent config files generated by brainclaw — `CLAUDE.md`, `AGENTS.md`, `GEMINI.md`, `.cursor/rules/brainclaw.md`, `.windsurfrules`, `.github/copilot-instructions.md` — are **not committed to Git**.
152
165
 
@@ -12,12 +12,12 @@ brainclaw export --format claude-md --write
12
12
 
13
13
  ## Recommended approach
14
14
 
15
- - add lightweight usage instructions for brainclaw in `CLAUDE.md`
16
- - use `.brainclaw/project.md` as a readable baseline
17
- - prefer MCP for dynamic retrieval when available
15
+ - use MCP as the default runtime path for dynamic retrieval and writes
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 baseline, not as the primary live source of truth
18
18
  - use hooks or workflow checks when a stronger reminder is needed
19
19
 
20
20
  ## Key idea
21
21
 
22
22
  Claude Code should not carry all workspace state in static instructions.
23
- brainclaw provides the living workspace layer.
23
+ brainclaw provides the living workspace layer through MCP plus local workflow guidance.
@@ -12,12 +12,12 @@ brainclaw export --format agents-md --write
12
12
 
13
13
  ## Recommended approach
14
14
 
15
- - keep a lightweight instruction telling Codex to consult brainclaw
16
- - let Codex read `.brainclaw/project.md` when simple file context is enough
17
- - use MCP for fresher scoped context when available
18
- - encourage use of plans, claims, and handoffs during multi-step work
15
+ - use MCP as the default runtime path for fresh context, plans, claims, and runtime notes
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 when MCP is unavailable or when a human wants a simple snapshot
18
+ - encourage plans, claims, and handoffs during multi-step work
19
19
 
20
20
  ## Good role for brainclaw here
21
21
 
22
22
  Codex stays the coding agent.
23
- brainclaw provides the shared workspace context and coordination layer.
23
+ brainclaw provides the live workspace context and coordination layer.
@@ -12,9 +12,10 @@ brainclaw export --format copilot-instructions --write
12
12
 
13
13
  ## Recommended approach
14
14
 
15
- - point Copilot to `.brainclaw/project.md` or use fresh context retrieval
15
+ - use MCP whenever the Copilot surface supports it for fresh context and coordination views
16
+ - keep `.github/copilot-instructions.md` lightweight and behavioral
17
+ - use `.brainclaw/project.md` as readable fallback, not as the only live context source
16
18
  - use plans, claims, and handoffs to reduce ambiguity across sessions
17
- - use MCP where supported for dynamic collaboration views
18
19
 
19
20
  ## Why this matters
20
21
 
@@ -12,12 +12,12 @@ brainclaw export --format cursor-rules --write
12
12
 
13
13
  ## Recommended approach
14
14
 
15
- - the generated `.cursor/rules/brainclaw.md` tells Cursor to consult brainclaw before significant edits
16
- - use `.brainclaw/project.md` for readable shared state
17
- - use MCP for dynamic retrieval when available
15
+ - use MCP as the default dynamic path for context, board state, plans, and claims
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 shared state
18
18
  - rely on claims and plans when multiple agents or humans are active in the same repo
19
19
 
20
20
  ## Key point
21
21
 
22
22
  Cursor rules describe behavior.
23
- brainclaw provides the living shared state those rules should point to.
23
+ brainclaw provides the living shared state those rules should point to through MCP.