brainclaw 1.8.0 → 1.9.1

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 (178) hide show
  1. package/README.md +592 -505
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli.js +138 -13
  4. package/dist/commands/add-step.js +1 -1
  5. package/dist/commands/bootstrap.js +2 -26
  6. package/dist/commands/check-security-mcp.js +50 -33
  7. package/dist/commands/check-security.js +86 -43
  8. package/dist/commands/claim.js +22 -21
  9. package/dist/commands/confirm.js +26 -0
  10. package/dist/commands/context-diff.js +1 -1
  11. package/dist/commands/dispatch-watch.js +142 -0
  12. package/dist/commands/doctor.js +113 -2
  13. package/dist/commands/estimation-report.js +115 -16
  14. package/dist/commands/harvest.js +286 -23
  15. package/dist/commands/hooks.js +73 -73
  16. package/dist/commands/init.js +124 -22
  17. package/dist/commands/install-hooks.js +78 -78
  18. package/dist/commands/loops-handlers.js +4 -0
  19. package/dist/commands/mcp-read-handlers.js +253 -41
  20. package/dist/commands/mcp.js +664 -102
  21. package/dist/commands/memory.js +21 -17
  22. package/dist/commands/migrate.js +81 -17
  23. package/dist/commands/prune.js +78 -4
  24. package/dist/commands/reflect.js +26 -20
  25. package/dist/commands/register-agent.js +57 -1
  26. package/dist/commands/repair.js +20 -0
  27. package/dist/commands/session-end.js +15 -6
  28. package/dist/commands/session-start.js +18 -1
  29. package/dist/commands/setup-security.js +39 -18
  30. package/dist/commands/setup.js +26 -27
  31. package/dist/commands/stale.js +16 -2
  32. package/dist/commands/switch.js +26 -5
  33. package/dist/commands/uninstall.js +126 -34
  34. package/dist/commands/update-step.js +6 -0
  35. package/dist/commands/version.js +1 -1
  36. package/dist/commands/worktree.js +60 -0
  37. package/dist/core/actions.js +12 -3
  38. package/dist/core/agent-capability.js +30 -17
  39. package/dist/core/agent-files.js +963 -666
  40. package/dist/core/agent-integrations.js +0 -3
  41. package/dist/core/agent-inventory.js +67 -0
  42. package/dist/core/agent-registry.js +163 -29
  43. package/dist/core/agentrun-reconciler.js +33 -2
  44. package/dist/core/agentruns.js +7 -1
  45. package/dist/core/ai-agent-detection.js +31 -44
  46. package/dist/core/archival.js +15 -9
  47. package/dist/core/assignment-reconciler.js +56 -0
  48. package/dist/core/assignment-sweeper.js +127 -4
  49. package/dist/core/assignments.js +69 -11
  50. package/dist/core/bootstrap.js +233 -67
  51. package/dist/core/brainclaw-version.js +22 -0
  52. package/dist/core/candidates.js +21 -1
  53. package/dist/core/claims.js +313 -150
  54. package/dist/core/codev-prompts.js +38 -38
  55. package/dist/core/config.js +6 -1
  56. package/dist/core/context-diff.js +148 -20
  57. package/dist/core/context.js +129 -8
  58. package/dist/core/coordination.js +22 -3
  59. package/dist/core/default-profiles/doctor.yaml +11 -11
  60. package/dist/core/default-profiles/janitor.yaml +11 -11
  61. package/dist/core/default-profiles/onboarder.yaml +11 -11
  62. package/dist/core/default-profiles/reviewer.yaml +13 -13
  63. package/dist/core/dispatch-status.js +79 -5
  64. package/dist/core/dispatcher.js +65 -12
  65. package/dist/core/entity-operations.js +74 -27
  66. package/dist/core/entity-registry.js +31 -5
  67. package/dist/core/event-log.js +138 -21
  68. package/dist/core/events/checkpoint.js +258 -0
  69. package/dist/core/events/genesis.js +220 -0
  70. package/dist/core/events/journal.js +507 -0
  71. package/dist/core/events/materialize.js +126 -0
  72. package/dist/core/events/registry-post-image.js +110 -0
  73. package/dist/core/events/verify.js +109 -0
  74. package/dist/core/execution-adapters.js +23 -0
  75. package/dist/core/execution.js +1 -1
  76. package/dist/core/facade-schema.js +38 -0
  77. package/dist/core/gc-semantic.js +130 -5
  78. package/dist/core/handoff-snapshot.js +68 -0
  79. package/dist/core/ids.js +19 -8
  80. package/dist/core/instruction-templates.js +34 -115
  81. package/dist/core/io.js +39 -3
  82. package/dist/core/json-store.js +10 -1
  83. package/dist/core/lock.js +153 -28
  84. package/dist/core/loops/bootstrap-acquire.js +25 -1
  85. package/dist/core/loops/facade-schema.js +2 -0
  86. package/dist/core/loops/hooks/survey-signals-baseline.js +36 -0
  87. package/dist/core/loops/index.js +1 -0
  88. package/dist/core/loops/presets/bootstrap.js +7 -0
  89. package/dist/core/loops/store.js +17 -0
  90. package/dist/core/loops/verbs.js +24 -2
  91. package/dist/core/markdown.js +8 -76
  92. package/dist/core/mcp-command-resolution.js +245 -0
  93. package/dist/core/memory-compactor.js +5 -3
  94. package/dist/core/memory-lifecycle.js +282 -0
  95. package/dist/core/merge-risk.js +150 -0
  96. package/dist/core/messaging.js +10 -3
  97. package/dist/core/migration.js +11 -1
  98. package/dist/core/observer-mode.js +26 -0
  99. package/dist/core/operations/memory-mutation.js +90 -65
  100. package/dist/core/operations/plan.js +27 -1
  101. package/dist/core/protocol-skills.js +210 -0
  102. package/dist/core/reflection-safety.js +6 -7
  103. package/dist/core/reputation.js +84 -2
  104. package/dist/core/runtime-signals.js +72 -10
  105. package/dist/core/runtime.js +84 -1
  106. package/dist/core/schema.js +114 -0
  107. package/dist/core/search.js +19 -2
  108. package/dist/core/security-detectors.js +125 -0
  109. package/dist/core/security-extract.js +189 -0
  110. package/dist/core/security-guard.js +217 -139
  111. package/dist/core/security-packages.js +121 -0
  112. package/dist/core/security-scoring.js +76 -9
  113. package/dist/core/security.js +34 -2
  114. package/dist/core/sequence.js +11 -2
  115. package/dist/core/setup-flow.js +141 -13
  116. package/dist/core/spawn-check.js +16 -2
  117. package/dist/core/staleness.js +73 -2
  118. package/dist/core/state.js +250 -54
  119. package/dist/core/store-resolution.js +45 -12
  120. package/dist/core/worktree.js +90 -26
  121. package/dist/facts.js +8 -8
  122. package/dist/facts.json +7 -7
  123. package/docs/PROTOCOL.md +223 -0
  124. package/docs/adapters/openclaw.md +43 -43
  125. package/docs/architecture/project-refs.md +328 -328
  126. package/docs/cli.md +2097 -2096
  127. package/docs/concepts/coordination.md +52 -52
  128. package/docs/concepts/coordinator-runbook.md +129 -0
  129. package/docs/concepts/dispatch-lifecycle.md +245 -245
  130. package/docs/concepts/event-log-store.md +928 -0
  131. package/docs/concepts/ideation-loop.md +317 -317
  132. package/docs/concepts/loop-engine.md +520 -511
  133. package/docs/concepts/mcp-governance.md +268 -268
  134. package/docs/concepts/memory.md +89 -88
  135. package/docs/concepts/multi-agent-workflows.md +167 -167
  136. package/docs/concepts/observer-protocol.md +361 -0
  137. package/docs/concepts/parallel-merge-protocol.md +71 -0
  138. package/docs/concepts/plans-and-claims.md +217 -174
  139. package/docs/concepts/project-md-convention.md +35 -35
  140. package/docs/concepts/runtime-notes.md +38 -38
  141. package/docs/concepts/skills.md +78 -0
  142. package/docs/concepts/troubleshooting.md +254 -254
  143. package/docs/concepts/workspace-bootstrapping.md +142 -81
  144. package/docs/context-format-changelog.md +35 -35
  145. package/docs/context-format.md +48 -48
  146. package/docs/index.md +65 -65
  147. package/docs/integrations/agents.md +162 -162
  148. package/docs/integrations/claude-code.md +23 -23
  149. package/docs/integrations/cline.md +87 -88
  150. package/docs/integrations/codex.md +2 -2
  151. package/docs/integrations/continue.md +60 -60
  152. package/docs/integrations/copilot.md +82 -80
  153. package/docs/integrations/cursor.md +23 -23
  154. package/docs/integrations/kilocode.md +72 -72
  155. package/docs/integrations/mcp.md +377 -377
  156. package/docs/integrations/mistral-vibe.md +122 -122
  157. package/docs/integrations/openclaw.md +99 -98
  158. package/docs/integrations/opencode.md +84 -84
  159. package/docs/integrations/overview.md +122 -122
  160. package/docs/integrations/roo.md +74 -74
  161. package/docs/integrations/windsurf.md +83 -83
  162. package/docs/mcp-schema-changelog.md +360 -329
  163. package/docs/playbooks/integration/index.md +121 -121
  164. package/docs/playbooks/orchestration.md +37 -0
  165. package/docs/playbooks/productivity/index.md +99 -99
  166. package/docs/playbooks/team/index.md +117 -117
  167. package/docs/product/agent-first-model.md +184 -184
  168. package/docs/product/entity-model-audit.md +462 -462
  169. package/docs/product/positioning.md +86 -86
  170. package/docs/quickstart-existing-project.md +107 -107
  171. package/docs/quickstart.md +148 -147
  172. package/docs/release-maintenance.md +79 -79
  173. package/docs/reputation.md +52 -52
  174. package/docs/review.md +45 -45
  175. package/docs/security.md +212 -53
  176. package/docs/server-operations.md +118 -118
  177. package/docs/storage.md +110 -108
  178. package/package.json +86 -69
@@ -9,9 +9,8 @@ import { buildAiSurfaceInventory, renderAiSurfaceUsageHints } from '../core/ai-s
9
9
  import { buildMachineProfile, saveMachineProfile, loadMachineProfile } from '../core/machine-profile.js';
10
10
  import { buildAgentInventory, saveAgentInventory, loadAgentInventory } from '../core/agent-inventory.js';
11
11
  import { ensureClaudeCodeUserSettings, ensureClaudeCodeUserCommand, ensureCursorMcpConfig, ensureWindsurfMcpConfig, ensureAntigravityMcpConfig, ensureContinueUserMcpConfig, ensureContinueUserPermissions, ensureCodexMcpConfig, ensureHermesMcpConfig, writeDetectedAgentAutoConfig, describeAutoConfigWrite, ensureGitignoreEntries, collectWorkspaceGitignoreEntries, BRAINCLAW_EXCLUSIVE_DIRECTORIES, } from '../core/agent-files.js';
12
- import { MEMORY_DIR, memoryExists, ensureMemoryDir } from '../core/io.js';
13
- import { saveConfig, defaultConfig } from '../core/config.js';
14
- import { readSetupState, resolveHomeDir, writeSetupState } from '../core/setup-state.js';
12
+ import { MEMORY_DIR, memoryExists } from '../core/io.js';
13
+ import { ensureUserStore, readSetupState, resolveHomeDir, writeSetupState } from '../core/setup-state.js';
15
14
  import { writeDetectedAgentHooks } from './hooks.js';
16
15
  export { readSetupState } from '../core/setup-state.js';
17
16
  // ─── Constants ────────────────────────────────────────────────────────────────
@@ -184,28 +183,11 @@ export function parseAgentSelection(choice, detected, installedAgents = []) {
184
183
  export function initUserStore(home, env = process.env) {
185
184
  if (!home)
186
185
  return [];
187
- const written = [];
188
- // Ensure ~/.brainclaw/ directory and subdirs exist
189
- const userStorePath = path.join(home, '.brainclaw');
190
- ensureMemoryDir(home);
191
- // Check if config.yaml already exists (idempotent)
192
- const configPath = path.join(userStorePath, 'config.yaml');
186
+ const configPath = path.join(home, '.brainclaw', 'config.yaml');
193
187
  if (fs.existsSync(configPath)) {
194
188
  return [];
195
189
  }
196
- try {
197
- // Write a minimal config.yaml for the user store
198
- const defaultCfg = defaultConfig('user-global');
199
- saveConfig(defaultCfg, home);
200
- // Append store_type: user to the config.yaml (pattern already used in tests)
201
- fs.appendFileSync(configPath, 'store_type: user\n');
202
- written.push(configPath);
203
- }
204
- catch (err) {
205
- // Non-fatal: if user store init fails, continue with agent setup
206
- console.warn(`Warning: failed to initialize user store at ${configPath}:`, err instanceof Error ? err.message : String(err));
207
- }
208
- return written;
190
+ return ensureUserStore(env) ? [configPath] : [];
209
191
  }
210
192
  export function runGlobalInstall(selectedAgents, env = process.env) {
211
193
  const home = resolveHomeDir(env);
@@ -507,11 +489,16 @@ export async function runSetup(options = {}) {
507
489
  });
508
490
  // Step 3: Repo selection
509
491
  let repoChoice;
492
+ let nonInteractiveDefault = false;
510
493
  if (options.repos) {
511
494
  repoChoice = options.repos;
512
495
  }
513
496
  else if (options.yes || !process.stdin.isTTY) {
514
- repoChoice = 'all';
497
+ // Blast-radius guard: never initialise every repo under the roots just
498
+ // because nobody could be asked. Non-interactive runs default to the
499
+ // current repo; widening to all requires an explicit --repos all.
500
+ repoChoice = 'current';
501
+ nonInteractiveDefault = true;
515
502
  }
516
503
  else {
517
504
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
@@ -525,6 +512,9 @@ export async function runSetup(options = {}) {
525
512
  const selectedRepos = parseRepoSelection(repoChoice, repos);
526
513
  if (selectedRepos.length === 0) {
527
514
  console.log('No repositories selected. Aborting.');
515
+ if (nonInteractiveDefault) {
516
+ console.log('Non-interactive setup defaults to the current directory\'s repo. Pass --repos all to initialise every repo under the roots.');
517
+ }
528
518
  return;
529
519
  }
530
520
  console.log(`\nSelected ${selectedRepos.length} repository(s).`);
@@ -585,10 +575,19 @@ function resolveVsixPath() {
585
575
  const distVsix = path.join(thisDir, '..', 'brainclaw-vscode.vsix');
586
576
  if (fs.existsSync(distVsix))
587
577
  return distVsix;
588
- // 2. Dev mode: vscode-extension/ directory
589
- const devVsix = path.join(thisDir, '..', '..', 'vscode-extension', 'brainclaw-vscode-0.1.0.vsix');
590
- if (fs.existsSync(devVsix))
591
- return devVsix;
578
+ // 2. Dev mode: pick the newest locally packaged extension.
579
+ const devDir = path.join(thisDir, '..', '..', 'vscode-extension');
580
+ if (fs.existsSync(devDir)) {
581
+ const candidates = fs.readdirSync(devDir)
582
+ .filter((name) => /^brainclaw-vscode-\d+\.\d+\.\d+\.vsix$/.test(name))
583
+ .map((name) => {
584
+ const filePath = path.join(devDir, name);
585
+ return { filePath, mtimeMs: fs.statSync(filePath).mtimeMs };
586
+ })
587
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
588
+ if (candidates[0])
589
+ return candidates[0].filePath;
590
+ }
592
591
  return undefined;
593
592
  }
594
593
  //# sourceMappingURL=setup.js.map
@@ -19,7 +19,12 @@ import { removeEntity, transitionEntity, } from '../core/entity-operations.js';
19
19
  import { listRuntimeNotes } from '../core/runtime.js';
20
20
  import { loadState } from '../core/state.js';
21
21
  import { detectStaleness, staleSummary, } from '../core/staleness.js';
22
- /** Resolve the canonical transition target (or removal) per entity kind. */
22
+ /**
23
+ * Resolve the canonical transition target (or removal) per entity kind.
24
+ * decision/constraint are intentionally absent: they are only flagged for
25
+ * dead related_paths (pln#557 step 2), and the fix is updating the paths via
26
+ * bclaw_update — not a lifecycle transition.
27
+ */
23
28
  const RESOLVE_ACTIONS = {
24
29
  plan: { kind: 'transition', entity: 'plan', to: 'dropped' },
25
30
  handoff: { kind: 'transition', entity: 'handoff', to: 'closed' },
@@ -31,7 +36,11 @@ function buildReport(cwd) {
31
36
  const state = loadState(cwd);
32
37
  const pending = listCandidates('pending', cwd);
33
38
  const notes = listRuntimeNotes(undefined, cwd);
34
- return detectStaleness(state.plan_items, state.known_traps, state.open_handoffs, pending, Date.now(), notes);
39
+ return detectStaleness(state.plan_items, state.known_traps, state.open_handoffs, pending, Date.now(), notes, {
40
+ decisions: state.recent_decisions,
41
+ constraints: state.active_constraints,
42
+ projectRoot: cwd ?? process.cwd(),
43
+ });
35
44
  }
36
45
  export function runStaleList(options = {}) {
37
46
  const report = buildReport(options.cwd);
@@ -60,6 +69,11 @@ export function runStaleResolve(id, options = {}) {
60
69
  process.exit(1);
61
70
  }
62
71
  const action = RESOLVE_ACTIONS[warning.entity];
72
+ if (!action) {
73
+ console.error(`Error: ${warning.entity} ${id} has no canonical stale-resolve action.`);
74
+ console.error(`Fix it directly instead: ${warning.suggested_action}`);
75
+ process.exit(1);
76
+ }
63
77
  try {
64
78
  if (action.kind === 'transition') {
65
79
  const result = transitionEntity(action.entity, id, action.to, cwd, 'resolved via brainclaw stale resolve');
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { loadActiveProject, saveActiveProject, clearActiveProject } from '../core/active-project.js';
3
- import { buildOperationalIdentity, loadCurrentSession, saveCurrentSession } from '../core/identity.js';
3
+ import { buildOperationalIdentity, loadCurrentSession, loadSessionById, saveCurrentSession } from '../core/identity.js';
4
4
  import { memoryExists } from '../core/io.js';
5
5
  import { resolveProjectRef } from '../core/store-resolution.js';
6
6
  import { resolveCrossProjectLinks, resolveProjectCwd } from '../core/cross-project.js';
@@ -44,10 +44,28 @@ export function switchProject(projectRef, options = {}) {
44
44
  catch { /* name is optional */ }
45
45
  const now = new Date().toISOString();
46
46
  const sessionOnly = options.sessionOnly ?? true;
47
- let session = loadCurrentSession(cwd);
47
+ let session = options.sessionId ? loadSessionById(options.sessionId, cwd) : loadCurrentSession(cwd);
48
48
  if (!session && sessionOnly) {
49
- buildOperationalIdentity(undefined, cwd, { persistImplicitSession: true });
50
- session = loadCurrentSession(cwd);
49
+ if (options.sessionId) {
50
+ const identity = buildOperationalIdentity(undefined, cwd, {
51
+ sessionId: options.sessionId,
52
+ persistImplicitSession: false,
53
+ });
54
+ saveCurrentSession({
55
+ session_id: options.sessionId,
56
+ started_at: now,
57
+ last_seen_at: now,
58
+ agent: identity.agent,
59
+ agent_id: identity.agent_id,
60
+ host_id: identity.host_id,
61
+ user: process.env.USER || process.env.USERNAME || undefined,
62
+ pid: process.pid,
63
+ }, cwd);
64
+ }
65
+ else {
66
+ buildOperationalIdentity(undefined, cwd, { persistImplicitSession: true });
67
+ }
68
+ session = options.sessionId ? loadSessionById(options.sessionId, cwd) : loadCurrentSession(cwd);
51
69
  }
52
70
  if (session && sessionOnly) {
53
71
  saveCurrentSession({
@@ -78,11 +96,14 @@ export function switchProject(projectRef, options = {}) {
78
96
  * List available projects in the workspace.
79
97
  */
80
98
  export function listAvailableProjects(cwd) {
99
+ return listAvailableProjectsForSession(cwd);
100
+ }
101
+ export function listAvailableProjectsForSession(cwd, sessionId) {
81
102
  const wsRoot = findOutermostWorkspaceRoot(cwd ?? process.cwd());
82
103
  if (!wsRoot) {
83
104
  throw new Error('No brainclaw workspace found.');
84
105
  }
85
- const sessionActive = loadCurrentSession(cwd)?.active_project;
106
+ const sessionActive = (sessionId ? loadSessionById(sessionId, cwd) : loadCurrentSession(cwd))?.active_project;
86
107
  const globalActive = loadActiveProject(wsRoot);
87
108
  const active = sessionActive ?? globalActive;
88
109
  const activeSource = sessionActive ? 'session' : globalActive ? 'global' : 'none';
@@ -1,9 +1,9 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import readline from 'node:readline/promises';
4
3
  import { MEMORY_DIR, memoryExists } from '../core/io.js';
5
4
  import { BRAINCLAW_SECTION_START, BRAINCLAW_SECTION_END, AGENT_EXPORT_REGISTRY, } from '../core/agent-files.js';
6
5
  import { resolveHomeDir } from '../core/setup-state.js';
6
+ import { confirmAction } from './confirm.js';
7
7
  /**
8
8
  * Remove brainclaw from a project and/or machine.
9
9
  *
@@ -29,19 +29,7 @@ async function uninstallProject(cwd, skipConfirm) {
29
29
  console.log('No .brainclaw/ found in this project. Nothing to uninstall.');
30
30
  return;
31
31
  }
32
- if (!skipConfirm && process.stdin.isTTY) {
33
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
34
- try {
35
- const answer = (await rl.question('Remove brainclaw from this project? This deletes .brainclaw/ and all generated agent files. [y/N]: ')).trim().toLowerCase();
36
- if (answer !== 'y' && answer !== 'yes') {
37
- console.log('Aborted.');
38
- return;
39
- }
40
- }
41
- finally {
42
- rl.close();
43
- }
44
- }
32
+ await confirmAction('Remove brainclaw from this project? This deletes .brainclaw/ and all generated agent files.', skipConfirm);
45
33
  // Remove .brainclaw/ directory
46
34
  const brainclawDir = path.join(cwd, MEMORY_DIR);
47
35
  if (fs.existsSync(brainclawDir)) {
@@ -88,17 +76,12 @@ async function uninstallProject(cwd, skipConfirm) {
88
76
  }
89
77
  }
90
78
  }
91
- // Remove companion config files
79
+ // Remove companion files dedicated to brainclaw. Shared JSON configs are
80
+ // stripped below so uninstall does not delete user-owned settings.
92
81
  const companionFiles = [
93
- '.mcp.json',
94
82
  '.claude/commands/brainclaw.md',
95
- '.claude/settings.local.json',
96
83
  '.claude/.bclaw-session',
97
84
  '.cursor/rules/brainclaw-mcp-shim.mdc',
98
- '.vscode/cline_mcp_settings.json',
99
- '.roo/mcp.json',
100
- '.continue/config.json',
101
- 'opencode.json',
102
85
  '.github/skills/brainclaw-context/SKILL.md',
103
86
  ];
104
87
  for (const relativePath of companionFiles) {
@@ -108,6 +91,7 @@ async function uninstallProject(cwd, skipConfirm) {
108
91
  console.log(`✔ Removed ${relativePath}`);
109
92
  }
110
93
  }
94
+ stripProjectCompanionConfigs(cwd);
111
95
  console.log('✔ Project uninstall complete.');
112
96
  }
113
97
  async function uninstallMachine(skipConfirm) {
@@ -121,19 +105,7 @@ async function uninstallMachine(skipConfirm) {
121
105
  console.log('No ~/.brainclaw/ found. Nothing to uninstall.');
122
106
  return;
123
107
  }
124
- if (!skipConfirm && process.stdin.isTTY) {
125
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
126
- try {
127
- const answer = (await rl.question('Remove brainclaw global config (~/.brainclaw/)? [y/N]: ')).trim().toLowerCase();
128
- if (answer !== 'y' && answer !== 'yes') {
129
- console.log('Aborted.');
130
- return;
131
- }
132
- }
133
- finally {
134
- rl.close();
135
- }
136
- }
108
+ await confirmAction('Remove brainclaw global config (~/.brainclaw/)?', skipConfirm);
137
109
  fs.rmSync(userStore, { recursive: true, force: true });
138
110
  console.log('✔ Removed ~/.brainclaw/');
139
111
  // Note: global MCP configs in ~/.claude/settings.json, ~/.cursor/mcp.json etc.
@@ -142,4 +114,124 @@ async function uninstallMachine(skipConfirm) {
142
114
  console.log('Remove brainclaw entries manually if needed.');
143
115
  console.log('✔ Machine uninstall complete.');
144
116
  }
117
+ function isJsonObject(value) {
118
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
119
+ }
120
+ function stripBrainclawKeyedMcp(config) {
121
+ const servers = isJsonObject(config.mcpServers) ? { ...config.mcpServers } : undefined;
122
+ if (!servers || !Object.prototype.hasOwnProperty.call(servers, 'brainclaw'))
123
+ return false;
124
+ delete servers.brainclaw;
125
+ if (Object.keys(servers).length === 0) {
126
+ delete config.mcpServers;
127
+ }
128
+ else {
129
+ config.mcpServers = servers;
130
+ }
131
+ return true;
132
+ }
133
+ function stripBrainclawContinueMcp(config) {
134
+ if (!Array.isArray(config.mcpServers))
135
+ return false;
136
+ const next = config.mcpServers.filter((entry) => !(isJsonObject(entry) && entry.name === 'brainclaw'));
137
+ if (next.length === config.mcpServers.length)
138
+ return false;
139
+ if (next.length === 0) {
140
+ delete config.mcpServers;
141
+ }
142
+ else {
143
+ config.mcpServers = next;
144
+ }
145
+ return true;
146
+ }
147
+ function stripBrainclawOpenCodeMcp(config) {
148
+ const mcp = isJsonObject(config.mcp) ? { ...config.mcp } : undefined;
149
+ if (!mcp || !Object.prototype.hasOwnProperty.call(mcp, 'brainclaw'))
150
+ return false;
151
+ delete mcp.brainclaw;
152
+ if (Object.keys(mcp).length === 0) {
153
+ delete config.mcp;
154
+ }
155
+ else {
156
+ config.mcp = mcp;
157
+ }
158
+ return true;
159
+ }
160
+ function stripBrainclawClaudePermissions(config, cwd) {
161
+ const permissions = isJsonObject(config.permissions) ? { ...config.permissions } : undefined;
162
+ if (!permissions)
163
+ return false;
164
+ let changed = false;
165
+ if (Array.isArray(permissions.allow)) {
166
+ const next = permissions.allow.filter((entry) => entry !== 'Bash(npx brainclaw:*)' && entry !== 'mcp__brainclaw__*');
167
+ changed = changed || next.length !== permissions.allow.length;
168
+ if (next.length === 0) {
169
+ delete permissions.allow;
170
+ }
171
+ else {
172
+ permissions.allow = next;
173
+ }
174
+ }
175
+ if (Array.isArray(permissions.additionalDirectories)) {
176
+ const brainclawDirs = new Set([
177
+ path.join(cwd, '.claude', 'worktrees'),
178
+ path.join(resolveHomeDir() ?? '', '.brainclaw', 'worktrees'),
179
+ ].filter(Boolean).map((entry) => path.resolve(entry).replace(/\\/g, '/').toLowerCase()));
180
+ const next = permissions.additionalDirectories.filter((entry) => {
181
+ if (typeof entry !== 'string')
182
+ return true;
183
+ return !brainclawDirs.has(path.resolve(entry).replace(/\\/g, '/').toLowerCase());
184
+ });
185
+ changed = changed || next.length !== permissions.additionalDirectories.length;
186
+ if (next.length === 0) {
187
+ delete permissions.additionalDirectories;
188
+ }
189
+ else {
190
+ permissions.additionalDirectories = next;
191
+ }
192
+ }
193
+ if (!changed)
194
+ return false;
195
+ if (Object.keys(permissions).length === 0) {
196
+ delete config.permissions;
197
+ }
198
+ else {
199
+ config.permissions = permissions;
200
+ }
201
+ return true;
202
+ }
203
+ function rewriteJsonConfig(relativePath, cwd, strip) {
204
+ const fullPath = path.join(cwd, relativePath);
205
+ if (!fs.existsSync(fullPath))
206
+ return;
207
+ let config;
208
+ try {
209
+ const parsed = JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
210
+ if (!isJsonObject(parsed))
211
+ return;
212
+ config = { ...parsed };
213
+ }
214
+ catch {
215
+ console.log(`! Skipped ${relativePath} (not valid JSON)`);
216
+ return;
217
+ }
218
+ if (!strip(config))
219
+ return;
220
+ if (Object.keys(config).length === 0) {
221
+ fs.unlinkSync(fullPath);
222
+ console.log(`✔ Removed ${relativePath} (was brainclaw-only)`);
223
+ }
224
+ else {
225
+ fs.writeFileSync(fullPath, `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
226
+ console.log(`✔ Removed brainclaw entries from ${relativePath}`);
227
+ }
228
+ }
229
+ function stripProjectCompanionConfigs(cwd) {
230
+ rewriteJsonConfig('.mcp.json', cwd, stripBrainclawKeyedMcp);
231
+ rewriteJsonConfig('.vscode/cline_mcp_settings.json', cwd, stripBrainclawKeyedMcp);
232
+ rewriteJsonConfig('.roo/mcp.json', cwd, stripBrainclawKeyedMcp);
233
+ rewriteJsonConfig('.continue/config.json', cwd, stripBrainclawContinueMcp);
234
+ rewriteJsonConfig('opencode.json', cwd, stripBrainclawOpenCodeMcp);
235
+ rewriteJsonConfig('.claude/settings.local.json', cwd, (config) => stripBrainclawClaudePermissions(config, cwd));
236
+ }
145
237
  //# sourceMappingURL=uninstall.js.map
@@ -14,6 +14,8 @@ export function runUpdateStep(planId, stepId, options) {
14
14
  status: options.status,
15
15
  text: options.text,
16
16
  assignee: options.assign,
17
+ estimatedEffort: options.estimate,
18
+ actualEffort: options.actualEffort,
17
19
  });
18
20
  const changes = [];
19
21
  if (options.status)
@@ -22,6 +24,10 @@ export function runUpdateStep(planId, stepId, options) {
22
24
  changes.push('text updated');
23
25
  if (options.assign !== undefined)
24
26
  changes.push(`assignee=${options.assign || 'unassigned'}`);
27
+ if (options.estimate !== undefined)
28
+ changes.push(`estimate=${options.estimate}`);
29
+ if (options.actualEffort !== undefined)
30
+ changes.push(`actual=${options.actualEffort}`);
25
31
  console.log(`✔ Step updated: [${result.stepId}] ${changes.join(', ')}`);
26
32
  console.log(` Plan [${result.planId}] progress: ${result.doneSteps}/${result.totalSteps} steps done`);
27
33
  if (result.planAutoCompleted) {
@@ -8,7 +8,7 @@ import { generateAgentReleaseNotes } from './release-notes.js';
8
8
  export function runVersion(options = {}) {
9
9
  const cwd = options.cwd ?? process.cwd();
10
10
  const initialized = memoryExists(cwd);
11
- let config = initialized ? loadConfig(cwd) : undefined;
11
+ const config = initialized ? loadConfig(cwd) : undefined;
12
12
  let publishedLocalRelease;
13
13
  if (options.publishLocal) {
14
14
  if (!initialized || !config) {
@@ -1,4 +1,5 @@
1
1
  import { createWorktree, listWorktrees, removeWorktree, pruneWorktrees, cleanMergedWorktrees, mergeWorktreeBranch, worktreesBaseDir, } from '../core/worktree.js';
2
+ import { analyzeMergeRisk } from '../core/merge-risk.js';
2
3
  import { memoryExists } from '../core/io.js';
3
4
  import { resolveTargetStore } from '../core/store-resolution.js';
4
5
  export function runWorktreeCreate(options) {
@@ -81,6 +82,20 @@ export function runWorktreeClean(options) {
81
82
  }
82
83
  export function runWorktreeMerge(options) {
83
84
  const cwd = resolveTargetStore(options.cwd ?? process.cwd(), options.store ?? 'local');
85
+ // Pre-merge conflict reflex (pln#396): warn if another live lane overlaps
86
+ // the files this branch would land. Advisory — never blocks the merge.
87
+ try {
88
+ const risk = analyzeMergeRisk(cwd, { branches: undefined });
89
+ const conflicting = risk.overlaps.filter(o => o.branches.includes(options.branch));
90
+ if (conflicting.length > 0) {
91
+ console.warn(`⚠ Pre-merge risk: ${options.branch} overlaps ${conflicting.length} file(s) with other live lane(s):`);
92
+ for (const o of conflicting.slice(0, 10)) {
93
+ console.warn(` ${o.file} — also in ${o.branches.filter(b => b !== options.branch).join(', ')}`);
94
+ }
95
+ console.warn(' Run `brainclaw worktree check` for the full picture. Proceeding with merge.');
96
+ }
97
+ }
98
+ catch { /* advisory only — never block the merge on the risk probe */ }
84
99
  const result = mergeWorktreeBranch(cwd, options.branch, {
85
100
  message: options.message,
86
101
  dryRun: options.dryRun,
@@ -98,6 +113,51 @@ export function runWorktreeMerge(options) {
98
113
  console.log(`✔ Merged ${options.branch} → ${result.commitHash}`);
99
114
  console.log(` ${result.filesChanged} files changed, ${result.filesRestored} parasitic deletion(s) auto-restored.`);
100
115
  }
116
+ /**
117
+ * `brainclaw worktree check` (pln#396) — pre-merge conflict detection across
118
+ * the parallel lanes. Exit 0 when lanes are disjoint, 3 when overlaps exist
119
+ * (a distinct non-error code so a supervisor script can gate a batch merge
120
+ * without treating "risk found" as a crash).
121
+ */
122
+ export function runWorktreeCheck(options) {
123
+ const cwd = resolveTargetStore(options.cwd ?? process.cwd(), options.store ?? 'local');
124
+ const report = analyzeMergeRisk(cwd, { baseRef: options.baseRef });
125
+ if (options.json) {
126
+ console.log(JSON.stringify(report, null, 2));
127
+ if (report.has_risk)
128
+ process.exitCode = 3;
129
+ return;
130
+ }
131
+ printMergeRiskReport(report);
132
+ if (report.has_risk)
133
+ process.exitCode = 3;
134
+ }
135
+ function printMergeRiskReport(report) {
136
+ console.log(`Pre-merge conflict check (base: ${report.base_ref})`);
137
+ console.log(` ${report.summary}`);
138
+ if (report.lanes.length === 0)
139
+ return;
140
+ console.log('\nLanes:');
141
+ for (const lane of report.lanes) {
142
+ const who = lane.claim_id
143
+ ? `${lane.agent ?? 'unknown'} · claim ${lane.claim_id}`
144
+ : (lane.agent ?? lane.session_id ?? 'unclaimed');
145
+ const dirtyNote = lane.dirty_files.length ? ` (+${lane.dirty_files.length} uncommitted)` : '';
146
+ console.log(` • ${lane.branch} [${who}] — ${lane.changed_files.length} file(s)${dirtyNote}`);
147
+ }
148
+ if (report.overlaps.length > 0) {
149
+ console.log('\n⚠ Overlapping files (potential conflicts on merge):');
150
+ for (const o of report.overlaps) {
151
+ console.log(` ${o.file}`);
152
+ console.log(` lanes: ${o.branches.join(', ')}`);
153
+ if (o.claims.length)
154
+ console.log(` claims: ${o.claims.join(', ')}`);
155
+ }
156
+ console.log('\nMerge protocol: merge one overlapping lane, then rebase/re-check the others');
157
+ console.log('before merging them. Disjoint lanes can merge in any order. See');
158
+ console.log('docs/concepts/parallel-merge-protocol.md.');
159
+ }
160
+ }
101
161
  /** Returns WorktreeInfo[] for use by MCP tools. */
102
162
  export function getWorktrees(cwd) {
103
163
  return listWorktrees(cwd);
@@ -7,6 +7,7 @@ import { nowISO, generateIdWithLabel } from './ids.js';
7
7
  import { ActionRequiredSchema, } from './schema.js';
8
8
  import { saveVersionedJsonFile } from './migration.js';
9
9
  import { appendAuditEntry } from './audit.js';
10
+ import { emitRegistryPostImage, registryFaultPoint } from './events/registry-post-image.js';
10
11
  import { createRuntimeEvent } from './events.js';
11
12
  import { loadAssignment, transitionAssignment } from './assignments.js';
12
13
  import { loadAgentRun, transitionAgentRun } from './agentruns.js';
@@ -119,10 +120,12 @@ function expireStaleActions(actions, cwd) {
119
120
  }
120
121
  return actions;
121
122
  }
122
- export function listActionRequired(cwd, filter = {}) {
123
+ export function listActionRequired(cwd, filter = {}, options = {}) {
123
124
  let actions = actionStore(cwd).list();
124
125
  // Sweep-on-read: expire stale pending actions
125
- actions = expireStaleActions(actions, cwd);
126
+ if (options.expireStale !== false) {
127
+ actions = expireStaleActions(actions, cwd);
128
+ }
126
129
  if (filter.status)
127
130
  actions = actions.filter((action) => action.status === filter.status);
128
131
  if (filter.kind)
@@ -145,7 +148,13 @@ function saveActionRequired(action, cwd) {
145
148
  mutate({ cwd }, () => {
146
149
  ensureActionsDir(cwd);
147
150
  const filepath = path.join(actionsDir(cwd, 'write'), `${action.id}.json`);
148
- saveVersionedJsonFile('action_required', filepath, ActionRequiredSchema.parse(action));
151
+ const parsed = ActionRequiredSchema.parse(action);
152
+ // pln#568 (I2): journal the post-image BEFORE the projection write. The
153
+ // action family journals under item_type 'state' (the observer's slot).
154
+ const created = !fs.existsSync(filepath);
155
+ emitRegistryPostImage('action', parsed, { created, agent: parsed.agent, agent_id: parsed.agent_id, session_id: parsed.session_id, cwd });
156
+ registryFaultPoint('after_registry_journal');
157
+ saveVersionedJsonFile('action_required', filepath, parsed);
149
158
  });
150
159
  }
151
160
  export function createActionRequired(options, cwd) {
@@ -4,14 +4,12 @@
4
4
  * integration depth, and pressure level accordingly.
5
5
  *
6
6
  * Three profile tiers drive instruction file templates:
7
- * A (full) — MCP + hooks → lightweight instructions (context via hooks/MCP)
8
- * B (standard) — MCP, no hooks working rules + architecture + top traps
9
- * C (limited) — no MCP → rich static content (plans, traps, decisions)
7
+ * A (full) managed MCP/native surfaces → lightweight instructions
8
+ * B (standard) — MCP with fewer automation surfaces more directive rules
9
+ * C (limited) — no MCP → rich static content (plans, traps, decisions)
10
10
  *
11
- * Tier A agents (as of 2026-04): Claude Code, Copilot, Codex, Cursor,
12
- * Windsurf (12 hooks!), Cline (macOS/Linux only).
13
- * Note: Cline hooks don't work on Windows — but templateTier stays A
14
- * because brainclaw generates hooks that gracefully degrade.
11
+ * Hook support is declared separately through `hasHooks` and `runtime.hooks`.
12
+ * Do not infer hooks from `templateTier`.
15
13
  */
16
14
  import os from 'node:os';
17
15
  import path from 'node:path';
@@ -25,9 +23,21 @@ const AGENT_ALIASES = {
25
23
  'vibe': 'mistral-vibe',
26
24
  'hermes-agent': 'hermes',
27
25
  };
28
- /** Resolve an alias to its canonical agent name, or return the input unchanged. */
26
+ /**
27
+ * Resolve an alias to its canonical agent name (case-insensitive).
28
+ *
29
+ * Agent names are case-insensitive: every canonical profile key and every alias
30
+ * is lowercase, so we trim + lowercase the input before resolving. This is the
31
+ * single normalization point — registry, messaging, spawn-check and the
32
+ * coordinate/dispatch pre-flight all route through here, so a target like
33
+ * "Codex" or "Gemini" resolves identically to "codex" / "gemini". Without it the
34
+ * dispatch pre-flight collapsed an unresolved name into "no CLI spawn template
35
+ * (IDE-only?)" and silently dropped the reviewer (github-copilot/Gemini hit this
36
+ * passing targetAgents=["Codex"]).
37
+ */
29
38
  export function resolveAgentAlias(name) {
30
- return AGENT_ALIASES[name] ?? name;
39
+ const key = name.trim().toLowerCase();
40
+ return AGENT_ALIASES[key] ?? key;
31
41
  }
32
42
  const PROFILES = {
33
43
  // --- Code agents (interactive, IDE-driven) ---
@@ -77,20 +87,20 @@ const PROFILES = {
77
87
  },
78
88
  windsurf: {
79
89
  name: 'windsurf', category: 'code-agent', workflowModel: 'interactive',
80
- hasMcp: true, hasHooks: true, hasAutoApprove: false, hasSkills: true, hasRules: true,
90
+ hasMcp: true, hasHooks: false, hasAutoApprove: false, hasSkills: false, hasRules: true,
81
91
  instructionFile: '.windsurfrules', sharedInstructionFile: true, mcpConfigScope: 'machine', templateTier: 'A',
82
92
  role_capabilities: ['execute', 'review'],
83
- runtime: { mcp_direct: true, hooks: true, canBeSpawnedCli: false, canSpawnOtherCli: false, inbox: false },
93
+ runtime: { mcp_direct: true, hooks: false, canBeSpawnedCli: false, canSpawnOtherCli: false, inbox: false },
84
94
  max_concurrent_tasks: 1,
85
95
  prompt_delivery: { methods: ['inbox_structured'], preferred: 'inbox_structured' },
86
96
  execution_env: { surface: 'ide' },
87
97
  },
88
98
  cline: {
89
99
  name: 'cline', category: 'code-agent', workflowModel: 'interactive',
90
- hasMcp: true, hasHooks: true, hasAutoApprove: true, hasSkills: true, hasRules: true,
100
+ hasMcp: true, hasHooks: false, hasAutoApprove: true, hasSkills: false, hasRules: true,
91
101
  instructionFile: '.clinerules/brainclaw.md', sharedInstructionFile: false, mcpConfigScope: 'project', templateTier: 'A',
92
102
  role_capabilities: ['execute', 'review'],
93
- runtime: { mcp_direct: true, hooks: true, canBeSpawnedCli: true, canSpawnOtherCli: false, inbox: true },
103
+ runtime: { mcp_direct: true, hooks: false, canBeSpawnedCli: true, canSpawnOtherCli: false, inbox: true },
94
104
  max_concurrent_tasks: 3,
95
105
  prompt_delivery: { methods: ['inline_arg', 'inbox_structured'], preferred: 'inline_arg', max_inline_length: 8000 },
96
106
  execution_env: { surface: 'extension' },
@@ -143,10 +153,10 @@ const PROFILES = {
143
153
  // worktree. The coordinator then syncs them via bclaw_harvest_candidates.
144
154
  codex: {
145
155
  name: 'codex', category: 'code-agent', workflowModel: 'task-based',
146
- hasMcp: true, hasHooks: true, hasAutoApprove: false, hasSkills: true, hasRules: true,
156
+ hasMcp: true, hasHooks: false, hasAutoApprove: false, hasSkills: true, hasRules: true,
147
157
  instructionFile: 'AGENTS.md', sharedInstructionFile: true, mcpConfigScope: 'machine', templateTier: 'A',
148
158
  role_capabilities: ['execute', 'review'],
149
- runtime: { mcp_direct: true, hooks: true, canBeSpawnedCli: true, canSpawnOtherCli: false, inbox: true },
159
+ runtime: { mcp_direct: true, hooks: false, canBeSpawnedCli: true, canSpawnOtherCli: false, inbox: true },
150
160
  max_concurrent_tasks: 5,
151
161
  // pln#475: prefer stdin_pipe to avoid Windows cmd.exe arg-parsing breaking
152
162
  // long prompts. codex.cmd resolves through cmd shell, where embedded
@@ -316,7 +326,10 @@ const _customProfiles = new Map();
316
326
  * Custom profiles take precedence over DEFAULT_CAPABILITY_PROFILES on lookup.
317
327
  */
318
328
  export function registerCapabilityProfile(profile) {
319
- _customProfiles.set(profile.name, profile);
329
+ // Key by the normalized (lowercased) name so lookups through the
330
+ // case-insensitive resolveAgentAlias match regardless of the casing the
331
+ // registrant used.
332
+ _customProfiles.set(profile.name.trim().toLowerCase(), profile);
320
333
  }
321
334
  /**
322
335
  * Get the capability profile for an agent by name.
@@ -669,7 +682,7 @@ export function resolveBriefMode(agentName) {
669
682
  * pln#528 — capability matrix DERIVED from the spawn template, so it stays in
670
683
  * sync with how each agent is actually invoked (no per-profile duplication).
671
684
  *
672
- * The motivating reality (debrief LeaseUp): codex is spawned with
685
+ * The motivating reality (a cross-project field debrief): codex is spawned with
673
686
  * `--sandbox workspace-write`, which (a) does NOT wire the brainclaw MCP server
674
687
  * and (b) puts `.git` outside the sandbox root — so a sandboxed worker can
675
688
  * neither call `bclaw_*` nor `git commit`, regardless of the profile's nominal