brainclaw 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/README.md +12 -11
  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 +285 -22
  15. package/dist/commands/init.js +123 -21
  16. package/dist/commands/loops-handlers.js +4 -0
  17. package/dist/commands/mcp-read-handlers.js +198 -29
  18. package/dist/commands/mcp.js +588 -92
  19. package/dist/commands/memory.js +21 -17
  20. package/dist/commands/migrate.js +81 -17
  21. package/dist/commands/prune.js +78 -4
  22. package/dist/commands/reflect.js +26 -20
  23. package/dist/commands/register-agent.js +57 -1
  24. package/dist/commands/repair.js +20 -0
  25. package/dist/commands/session-end.js +15 -6
  26. package/dist/commands/session-start.js +18 -1
  27. package/dist/commands/setup-security.js +39 -18
  28. package/dist/commands/setup.js +26 -27
  29. package/dist/commands/stale.js +16 -2
  30. package/dist/commands/uninstall.js +126 -34
  31. package/dist/commands/update-step.js +6 -0
  32. package/dist/commands/worktree.js +60 -0
  33. package/dist/core/actions.js +12 -3
  34. package/dist/core/agent-capability.js +11 -13
  35. package/dist/core/agent-files.js +844 -547
  36. package/dist/core/agent-integrations.js +0 -3
  37. package/dist/core/agent-inventory.js +67 -0
  38. package/dist/core/agent-registry.js +163 -29
  39. package/dist/core/agentrun-reconciler.js +33 -2
  40. package/dist/core/agentruns.js +7 -1
  41. package/dist/core/ai-agent-detection.js +31 -44
  42. package/dist/core/archival.js +15 -9
  43. package/dist/core/assignment-reconciler.js +56 -0
  44. package/dist/core/assignment-sweeper.js +127 -4
  45. package/dist/core/assignments.js +69 -11
  46. package/dist/core/bootstrap.js +233 -67
  47. package/dist/core/brainclaw-version.js +22 -0
  48. package/dist/core/candidates.js +21 -1
  49. package/dist/core/claims.js +313 -150
  50. package/dist/core/config.js +6 -1
  51. package/dist/core/context-diff.js +148 -20
  52. package/dist/core/context.js +129 -8
  53. package/dist/core/coordination.js +22 -3
  54. package/dist/core/dispatch-status.js +79 -5
  55. package/dist/core/dispatcher.js +64 -11
  56. package/dist/core/entity-operations.js +45 -24
  57. package/dist/core/entity-registry.js +31 -5
  58. package/dist/core/event-log.js +138 -21
  59. package/dist/core/events/checkpoint.js +258 -0
  60. package/dist/core/events/genesis.js +220 -0
  61. package/dist/core/events/journal.js +507 -0
  62. package/dist/core/events/materialize.js +126 -0
  63. package/dist/core/events/registry-post-image.js +110 -0
  64. package/dist/core/events/verify.js +109 -0
  65. package/dist/core/execution-adapters.js +23 -0
  66. package/dist/core/facade-schema.js +38 -0
  67. package/dist/core/gc-semantic.js +130 -5
  68. package/dist/core/handoff-snapshot.js +68 -0
  69. package/dist/core/ids.js +19 -8
  70. package/dist/core/instruction-templates.js +34 -115
  71. package/dist/core/io.js +39 -3
  72. package/dist/core/json-store.js +10 -1
  73. package/dist/core/lock.js +153 -28
  74. package/dist/core/loops/bootstrap-acquire.js +25 -1
  75. package/dist/core/loops/facade-schema.js +2 -0
  76. package/dist/core/loops/hooks/survey-signals-baseline.js +36 -0
  77. package/dist/core/loops/index.js +1 -0
  78. package/dist/core/loops/presets/bootstrap.js +7 -0
  79. package/dist/core/loops/store.js +17 -0
  80. package/dist/core/loops/verbs.js +24 -1
  81. package/dist/core/markdown.js +8 -76
  82. package/dist/core/mcp-command-resolution.js +245 -0
  83. package/dist/core/memory-compactor.js +5 -3
  84. package/dist/core/memory-lifecycle.js +282 -0
  85. package/dist/core/merge-risk.js +150 -0
  86. package/dist/core/messaging.js +8 -1
  87. package/dist/core/migration.js +11 -1
  88. package/dist/core/observer-mode.js +26 -0
  89. package/dist/core/operations/memory-mutation.js +90 -65
  90. package/dist/core/operations/plan.js +27 -1
  91. package/dist/core/protocol-skills.js +210 -0
  92. package/dist/core/reflection-safety.js +6 -7
  93. package/dist/core/reputation.js +84 -2
  94. package/dist/core/runtime-signals.js +71 -9
  95. package/dist/core/runtime.js +84 -1
  96. package/dist/core/schema.js +114 -0
  97. package/dist/core/security-detectors.js +125 -0
  98. package/dist/core/security-extract.js +189 -0
  99. package/dist/core/security-guard.js +107 -29
  100. package/dist/core/security-packages.js +121 -0
  101. package/dist/core/security-scoring.js +76 -9
  102. package/dist/core/security.js +34 -2
  103. package/dist/core/sequence.js +11 -2
  104. package/dist/core/setup-flow.js +141 -13
  105. package/dist/core/staleness.js +72 -1
  106. package/dist/core/state.js +250 -54
  107. package/dist/core/store-resolution.js +19 -5
  108. package/dist/core/worktree.js +72 -8
  109. package/dist/facts.js +8 -8
  110. package/dist/facts.json +7 -7
  111. package/docs/PROTOCOL.md +223 -0
  112. package/docs/cli.md +11 -10
  113. package/docs/concepts/coordinator-runbook.md +129 -0
  114. package/docs/concepts/event-log-store-critique-A.md +333 -0
  115. package/docs/concepts/event-log-store-critique-B.md +353 -0
  116. package/docs/concepts/event-log-store-phase0-measurements.md +58 -0
  117. package/docs/concepts/event-log-store-proposal-A.md +365 -0
  118. package/docs/concepts/event-log-store-proposal-B.md +404 -0
  119. package/docs/concepts/event-log-store.md +928 -0
  120. package/docs/concepts/identity-model-proposal.md +371 -0
  121. package/docs/concepts/memory.md +5 -4
  122. package/docs/concepts/observer-protocol.md +361 -0
  123. package/docs/concepts/parallel-merge-protocol.md +71 -0
  124. package/docs/concepts/plans-and-claims.md +43 -0
  125. package/docs/concepts/skills.md +78 -0
  126. package/docs/concepts/workspace-bootstrapping.md +61 -0
  127. package/docs/integrations/agents.md +4 -4
  128. package/docs/integrations/cline.md +10 -11
  129. package/docs/integrations/codex.md +2 -2
  130. package/docs/integrations/continue.md +5 -5
  131. package/docs/integrations/copilot.md +14 -12
  132. package/docs/integrations/openclaw.md +7 -6
  133. package/docs/integrations/overview.md +7 -7
  134. package/docs/integrations/roo.md +3 -3
  135. package/docs/integrations/windsurf.md +6 -6
  136. package/docs/mcp-schema-changelog.md +29 -2
  137. package/docs/quickstart.md +48 -47
  138. package/docs/security.md +174 -15
  139. package/docs/storage.md +4 -2
  140. package/package.json +8 -6
@@ -1,6 +1,6 @@
1
1
  import { loadConfig } from '../core/config.js';
2
2
  import { memoryExists } from '../core/io.js';
3
- import { loadState, persistState } from '../core/state.js';
3
+ import { loadState, persistState, mutateState } from '../core/state.js';
4
4
  import { scanText } from '../core/security.js';
5
5
  import { validateCliInput } from '../core/input-validation.js';
6
6
  import { resolveTargetStore } from '../core/store-resolution.js';
@@ -229,26 +229,30 @@ function runMemoryDelete(id, cwd) {
229
229
  console.error('Error: .brainclaw/ not found. Run `brainclaw init` first.');
230
230
  process.exit(1);
231
231
  }
232
- const state = loadState(cwd);
233
- const arrays = [
234
- state.recent_decisions,
235
- state.active_constraints,
236
- state.known_traps,
237
- state.open_handoffs,
238
- ];
232
+ // mutateState (RMW under the store lock + deleteMissing) so the entity file
233
+ // is actually unlinked — persistState alone no longer deletes absent records.
239
234
  let deleted;
240
- for (const items of arrays) {
241
- const index = items.findIndex((item) => item.id === id || item.short_label === id);
242
- if (index >= 0) {
243
- [deleted] = items.splice(index, 1);
244
- break;
245
- }
235
+ try {
236
+ deleted = mutateState((state) => {
237
+ const arrays = [
238
+ state.recent_decisions,
239
+ state.active_constraints,
240
+ state.known_traps,
241
+ state.open_handoffs,
242
+ ];
243
+ for (const items of arrays) {
244
+ const index = items.findIndex((item) => item.id === id || item.short_label === id);
245
+ if (index >= 0) {
246
+ return items.splice(index, 1)[0];
247
+ }
248
+ }
249
+ throw new Error(`Memory item '${id}' not found.`);
250
+ }, cwd);
246
251
  }
247
- if (!deleted) {
248
- console.error(`Error: Memory item '${id}' not found.`);
252
+ catch (err) {
253
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
249
254
  process.exit(1);
250
255
  }
251
- persistState(state, cwd);
252
256
  console.log(`✔ Memory item deleted: [${deleted.id}] ${deleted.text}`);
253
257
  }
254
258
  function collectMemoryItems(state) {
@@ -1,21 +1,69 @@
1
- import { loadState, persistState } from '../core/state.js';
1
+ import { loadState, mutateState } from '../core/state.js';
2
2
  import { memoryExists } from '../core/io.js';
3
3
  import { resolveTargetStore } from '../core/store-resolution.js';
4
4
  import { appendAuditEntry } from '../core/audit.js';
5
5
  import { resolveCurrentAgentName } from '../core/agent-registry.js';
6
+ import { loadConfig, saveConfig } from '../core/config.js';
7
+ import { runGenesisMigration, runRegistryGenesisSupplement } from '../core/events/genesis.js';
6
8
  export function runMigrate(options = {}) {
7
9
  const cwd = options.cwd ?? process.cwd();
8
10
  if (!memoryExists(cwd)) {
9
11
  console.error('Error: .brainclaw/ not found. Run `brainclaw init` first.');
10
12
  process.exit(1);
11
13
  }
12
- if (options.promoteMachineItems) {
14
+ if (options.enableJournal) {
15
+ enableJournalMode(cwd, options.dryRun ?? false);
16
+ }
17
+ else if (options.promoteMachineItems) {
13
18
  promoteMachineItems(cwd, options.dryRun ?? false);
14
19
  }
15
20
  else {
16
- console.log('Usage: brainclaw migrate --promote-machine-items [--dry-run]');
17
- console.log('');
18
- console.log('Moves items tagged scope:machine from project store to user store (~/.brainclaw/).');
21
+ console.log('Usage:');
22
+ console.log(' brainclaw migrate --promote-machine-items [--dry-run]');
23
+ console.log(' Move items tagged scope:machine from project store to user store (~/.brainclaw/).');
24
+ console.log(' brainclaw migrate --enable-journal [--dry-run]');
25
+ console.log(' Turn on the event journal (mode=dual) for this existing store and backfill it (pln#567).');
26
+ }
27
+ }
28
+ /**
29
+ * pln#567 (decision A+D) — enable the event journal on an EXISTING store. New
30
+ * stores get this from `init`; this is the explicit opt-in for stores created
31
+ * before the cutover. Sets `store.journal.mode=dual` (an explicit user action,
32
+ * so it overrides a prior off) THEN runs genesis so the journal carries the
33
+ * full history rather than only mutations from now on (idempotent — a second
34
+ * run no-ops once a genesis note exists).
35
+ */
36
+ function enableJournalMode(cwd, dryRun) {
37
+ const config = loadConfig(cwd);
38
+ const currentMode = config.store?.journal?.mode ?? 'unset';
39
+ if (dryRun) {
40
+ const planned = runGenesisMigration({ cwd, dryRun: true });
41
+ const plannedRegistry = runRegistryGenesisSupplement({ cwd, dryRun: true });
42
+ console.log(`(dry-run) Would set store.journal.mode=dual (currently ${currentMode}), backfill ${planned.backfilled} memory entit(y/ies) and ${plannedRegistry.backfilled} registry entit(y/ies), and emit the registry cutover marker (pln#568).`);
43
+ return;
44
+ }
45
+ config.store = { ...config.store, journal: { ...config.store?.journal, mode: 'dual' } };
46
+ saveConfig(config, cwd);
47
+ // Config is written first so genesis (which checks resolveJournalMode) seeds
48
+ // under dual and the journal stays consistent with subsequent dual-writes.
49
+ const result = runGenesisMigration({ cwd });
50
+ if (result.status === 'already_present') {
51
+ console.log(`✔ store.journal.mode=dual. Memory journal already seeded (genesis present).`);
52
+ }
53
+ else {
54
+ console.log(`✔ store.journal.mode=dual and seeded: ${result.backfilled} memory entit(y/ies) backfilled across ${Object.keys(result.per_family ?? {}).length} families.`);
55
+ }
56
+ // pln#568 slice 3 — registry cutover: backfill the registry/coordination
57
+ // families and emit the `registry_genesis` marker so the observer can trust
58
+ // the journal as authoritative for claims/assignments/runs/actions (drop the
59
+ // board_summary MCP seed). Incremental + idempotent; safe to re-run to upgrade
60
+ // a store that was journal-enabled before this slice landed.
61
+ const registry = runRegistryGenesisSupplement({ cwd });
62
+ if (registry.status === 'already_present') {
63
+ console.log(`✔ Registry cutover marker already present — observer authority unchanged.`);
64
+ }
65
+ else {
66
+ console.log(`✔ Registry cutover: ${registry.backfilled} registry entit(y/ies) backfilled, observer can now trust the journal for coordination counts (pln#568).`);
19
67
  }
20
68
  }
21
69
  function promoteMachineItems(cwd, dryRun) {
@@ -49,27 +97,43 @@ function promoteMachineItems(cwd, dryRun) {
49
97
  console.error('Error: cannot resolve user store. Run `brainclaw setup` first.');
50
98
  process.exit(1);
51
99
  }
52
- const userState = loadState(userCwd);
53
- // Move constraints
100
+ const movedIds = {
101
+ constraints: new Set(machineConstraints.map((c) => c.id)),
102
+ decisions: new Set(machineDecisions.map((d) => d.id)),
103
+ traps: new Set(machineTraps.map((t) => t.id)),
104
+ };
105
+ // Write target FIRST (a crash here leaves a duplicate, never silent loss),
106
+ // each side as an atomic locked RMW so concurrent writes are not clobbered.
107
+ mutateState((userState) => {
108
+ for (const c of machineConstraints) {
109
+ if (!userState.active_constraints.some((x) => x.id === c.id))
110
+ userState.active_constraints.push(c);
111
+ }
112
+ for (const d of machineDecisions) {
113
+ if (!userState.recent_decisions.some((x) => x.id === d.id))
114
+ userState.recent_decisions.push(d);
115
+ }
116
+ for (const t of machineTraps) {
117
+ if (!userState.known_traps.some((x) => x.id === t.id))
118
+ userState.known_traps.push(t);
119
+ }
120
+ }, userCwd);
121
+ // Then delete from source — mutateState persists with deleteMissing so the
122
+ // promoted entity files are actually unlinked from the project store.
123
+ mutateState((sourceState) => {
124
+ sourceState.active_constraints = sourceState.active_constraints.filter((x) => !movedIds.constraints.has(x.id));
125
+ sourceState.recent_decisions = sourceState.recent_decisions.filter((x) => !movedIds.decisions.has(x.id));
126
+ sourceState.known_traps = sourceState.known_traps.filter((x) => !movedIds.traps.has(x.id));
127
+ }, cwd);
54
128
  for (const c of machineConstraints) {
55
- userState.active_constraints.push(c);
56
- state.active_constraints = state.active_constraints.filter((x) => x.id !== c.id);
57
129
  appendAuditEntry({ actor: agent, action: 'update', item_id: c.id, item_type: 'constraint', reason: 'promote to user store (machine scope)' }, cwd);
58
130
  }
59
- // Move decisions
60
131
  for (const d of machineDecisions) {
61
- userState.recent_decisions.push(d);
62
- state.recent_decisions = state.recent_decisions.filter((x) => x.id !== d.id);
63
132
  appendAuditEntry({ actor: agent, action: 'update', item_id: d.id, item_type: 'decision', reason: 'promote to user store (machine scope)' }, cwd);
64
133
  }
65
- // Move traps
66
134
  for (const t of machineTraps) {
67
- userState.known_traps.push(t);
68
- state.known_traps = state.known_traps.filter((x) => x.id !== t.id);
69
135
  appendAuditEntry({ actor: agent, action: 'update', item_id: t.id, item_type: 'trap', reason: 'promote to user store (machine scope)' }, cwd);
70
136
  }
71
- persistState(userState, userCwd);
72
- persistState(state, cwd);
73
137
  console.log(`\n✔ Promoted ${total} item(s) to user store (${userCwd})`);
74
138
  }
75
139
  //# sourceMappingURL=migrate.js.map
@@ -1,9 +1,11 @@
1
- import { loadState, saveState } from '../core/state.js';
2
- import { memoryExists } from '../core/io.js';
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { loadState, persistState } from '../core/state.js';
4
+ import { memoryExists, resolveEntityDir } from '../core/io.js';
3
5
  import { mutate } from '../core/mutation-pipeline.js';
4
6
  import { rebuildProjectMd } from '../core/markdown.js';
5
7
  import { deleteRuntimeNote, listRuntimeNotes } from '../core/runtime.js';
6
- import { expireStaleActiveClaims } from '../core/claims.js';
8
+ import { expireStaleActiveClaims, isClaimExpired, listClaims } from '../core/claims.js';
7
9
  import { archiveStalePlansAndHandoffs } from '../core/archival.js';
8
10
  import { rotateAuditLogIfNeeded } from '../core/audit.js';
9
11
  import { analyzeMemory, analyzeAndApply, formatReport } from '../core/memory-compactor.js';
@@ -35,6 +37,10 @@ export function runPrune(options = {}) {
35
37
  }
36
38
  // Original prune logic
37
39
  const now = new Date().toISOString();
40
+ if (options.dryRun) {
41
+ previewPrune(cwd, now, options);
42
+ return;
43
+ }
38
44
  let prunedCount = 0;
39
45
  let expiredClaimsCount = 0;
40
46
  let expiredNotesCount = 0;
@@ -48,7 +54,9 @@ export function runPrune(options = {}) {
48
54
  }
49
55
  state.active_constraints = state.active_constraints.filter(c => c.status !== 'expired');
50
56
  prunedCount = originalLength - state.active_constraints.length;
51
- saveState(state, cwd);
57
+ // deleteMissing: this RMW is atomic (loadState above runs under the same
58
+ // mutate() lock), so removing pruned files here cannot clobber concurrent writes.
59
+ persistState(state, cwd, { writeProjectMarkdown: false, deleteMissing: true });
52
60
  expiredClaimsCount = expireStaleActiveClaims(cwd);
53
61
  if (options.expired) {
54
62
  const notes = listRuntimeNotes(undefined, cwd);
@@ -83,4 +91,70 @@ export function runPrune(options = {}) {
83
91
  console.log(`✔ Pruned ${prunedCount} expired constraints, ${expiredClaimsCount} expired claims${archiveMsg}${rotateMsg}.`);
84
92
  }
85
93
  }
94
+ function previewPrune(cwd, now, options) {
95
+ const state = loadState(cwd);
96
+ const expiredConstraints = state.active_constraints.filter((c) => c.status === 'expired' || (c.status === 'active' && c.expires_at && c.expires_at < now));
97
+ const expiredClaims = listClaims(cwd).filter((claim) => claim.status === 'active' && isClaimExpired(claim));
98
+ const expiredNotes = options.expired
99
+ ? listRuntimeNotes(undefined, cwd).filter((note) => note.expires_at && note.expires_at < now)
100
+ : [];
101
+ const archivePreview = options.archive ? previewArchive(cwd) : [];
102
+ console.log('Dry run: no files will be changed.');
103
+ console.log(`Would prune ${expiredConstraints.length} expired constraints.`);
104
+ for (const constraint of expiredConstraints) {
105
+ console.log(` - constraint ${constraint.id}: ${constraint.text.slice(0, 80)}`);
106
+ }
107
+ console.log(`Would release ${expiredClaims.length} expired claims.`);
108
+ for (const claim of expiredClaims) {
109
+ console.log(` - claim ${claim.id}: ${claim.scope}`);
110
+ }
111
+ if (options.expired) {
112
+ console.log(`Would delete ${expiredNotes.length} expired runtime notes.`);
113
+ for (const note of expiredNotes) {
114
+ console.log(` - runtime note ${note.id}: ${note.agent}`);
115
+ }
116
+ }
117
+ if (options.archive) {
118
+ const total = archivePreview.reduce((sum, item) => sum + item.ids.length, 0);
119
+ console.log(`Would archive ${total} stale plans/handoffs.`);
120
+ for (const item of archivePreview) {
121
+ for (const id of item.ids) {
122
+ console.log(` - ${item.entity} ${id}`);
123
+ }
124
+ }
125
+ }
126
+ }
127
+ function previewArchive(cwd) {
128
+ const maxAgeMs = 30 * 24 * 60 * 60 * 1000;
129
+ const cutoff = new Date(Date.now() - maxAgeMs).toISOString();
130
+ return [
131
+ {
132
+ entity: 'plans',
133
+ ids: listArchiveEligibleIds(cwd, 'plans', cutoff, (item) => item.status === 'done' || item.status === 'dropped'),
134
+ },
135
+ {
136
+ entity: 'handoffs',
137
+ ids: listArchiveEligibleIds(cwd, 'handoffs', cutoff, (item) => item.status === 'closed'),
138
+ },
139
+ ].filter((entry) => entry.ids.length > 0);
140
+ }
141
+ function listArchiveEligibleIds(cwd, entity, cutoffDate, isEligible) {
142
+ const dir = resolveEntityDir(entity, cwd, 'read');
143
+ if (!fs.existsSync(dir))
144
+ return [];
145
+ const ids = [];
146
+ for (const file of fs.readdirSync(dir).filter((entry) => entry.endsWith('.json') && entry !== 'archive.json')) {
147
+ try {
148
+ const item = JSON.parse(fs.readFileSync(path.join(dir, file), 'utf-8'));
149
+ const date = (item.completed_at ?? item.updated_at ?? item.created_at);
150
+ if (!isEligible(item))
151
+ continue;
152
+ if (date && date > cutoffDate)
153
+ continue;
154
+ ids.push(typeof item.id === 'string' ? item.id : path.basename(file, '.json'));
155
+ }
156
+ catch { /* ignore malformed files in preview */ }
157
+ }
158
+ return ids;
159
+ }
86
160
  //# sourceMappingURL=prune.js.map
@@ -54,31 +54,37 @@ function runReflectBatchFromFile(filepath, baseOptions) {
54
54
  : [parsed];
55
55
  let created = 0;
56
56
  for (const rawEvent of rawEvents) {
57
+ // Only event-shape parse errors are skippable here. Errors raised by
58
+ // createCandidateFromInput (identity resolution, strict security blocks)
59
+ // MUST propagate: swallowing them silently dropped sensitive content and
60
+ // exited 0, defeating strict-import enforcement (pln#572).
61
+ let event;
57
62
  try {
58
- const event = RuntimeEventSchema.parse(rawEvent);
59
- if (!isReflectableRuntimeEvent(event)) {
60
- continue;
61
- }
62
- const candidateType = event.candidate_type ?? mapEventTypeToCandidateType(event.event_type);
63
- createCandidateFromInput(event.text, candidateType, {
64
- ...baseOptions,
65
- type: candidateType,
66
- tag: event.tags.length ? event.tags : baseOptions.tag,
67
- authorId: baseOptions.authorId ?? event.agent_id,
68
- projectId: baseOptions.projectId ?? event.project_id,
69
- hostId: baseOptions.hostId ?? event.host_id,
70
- sessionId: baseOptions.sessionId ?? event.session_id,
71
- source: baseOptions.source ?? event.agent,
72
- severity: baseOptions.severity ?? event.severity,
73
- from: baseOptions.from ?? event.from,
74
- to: baseOptions.to ?? event.to,
75
- path: baseOptions.path ?? event.related_paths?.[0],
76
- }, false, true, true);
77
- created++;
63
+ event = RuntimeEventSchema.parse(rawEvent);
78
64
  }
79
65
  catch {
80
66
  // skip malformed event records
67
+ continue;
68
+ }
69
+ if (!isReflectableRuntimeEvent(event)) {
70
+ continue;
81
71
  }
72
+ const candidateType = event.candidate_type ?? mapEventTypeToCandidateType(event.event_type);
73
+ createCandidateFromInput(event.text, candidateType, {
74
+ ...baseOptions,
75
+ type: candidateType,
76
+ tag: event.tags.length ? event.tags : baseOptions.tag,
77
+ authorId: baseOptions.authorId ?? event.agent_id,
78
+ projectId: baseOptions.projectId ?? event.project_id,
79
+ hostId: baseOptions.hostId ?? event.host_id,
80
+ sessionId: baseOptions.sessionId ?? event.session_id,
81
+ source: baseOptions.source ?? event.agent,
82
+ severity: baseOptions.severity ?? event.severity,
83
+ from: baseOptions.from ?? event.from,
84
+ to: baseOptions.to ?? event.to,
85
+ path: baseOptions.path ?? event.related_paths?.[0],
86
+ }, false, true, true);
87
+ created++;
82
88
  }
83
89
  console.log(`✔ Created ${created} candidate(s) from batch file`);
84
90
  }
@@ -1,4 +1,4 @@
1
- import { registerAgentIdentity, setCurrentAgentIdentity } from '../core/agent-registry.js';
1
+ import { listDebrisAgentIdentities, registerAgentIdentity, removeAgentIdentity, setCurrentAgentIdentity, } from '../core/agent-registry.js';
2
2
  import { memoryExists } from '../core/io.js';
3
3
  export function runRegisterAgent(agentName, options = {}) {
4
4
  if (!memoryExists()) {
@@ -26,4 +26,60 @@ export function runRegisterAgent(agentName, options = {}) {
26
26
  const fingerprintLabel = agent.identity_key ? `, fp=${agent.identity_key.fingerprint.slice(0, 12)}` : '';
27
27
  console.log(`✔ Agent registered: ${agent.agent_name} (${agent.agent_id}, kind=${agent.kind}${capabilitiesLabel}${fingerprintLabel})${currentLabel}`);
28
28
  }
29
+ /**
30
+ * Guarded identity removal (pln#562 step 2). Without --force, only known
31
+ * debris identities (test fixtures, alias leftovers) can be removed.
32
+ */
33
+ export function runRemoveAgent(agentNameOrId, options = {}) {
34
+ if (!memoryExists()) {
35
+ console.error('Error: .brainclaw/ not found. Run `brainclaw init` first.');
36
+ process.exit(1);
37
+ }
38
+ try {
39
+ const removed = removeAgentIdentity(agentNameOrId, { force: options.force });
40
+ if (options.json) {
41
+ console.log(JSON.stringify({ removed: true, agent: removed }, null, 2));
42
+ return;
43
+ }
44
+ console.log(`✔ Agent identity removed: ${removed.agent_name} (${removed.agent_id})`);
45
+ }
46
+ catch (err) {
47
+ const message = err instanceof Error ? err.message : String(err);
48
+ if (options.json) {
49
+ console.log(JSON.stringify({ removed: false, error: message }, null, 2));
50
+ }
51
+ else {
52
+ console.error(`✖ ${message}`);
53
+ }
54
+ process.exitCode = 1;
55
+ }
56
+ }
57
+ /** List identities flagged as registration debris, without removing anything. */
58
+ export function runListDebrisAgents(options = {}) {
59
+ if (!memoryExists()) {
60
+ console.error('Error: .brainclaw/ not found. Run `brainclaw init` first.');
61
+ process.exit(1);
62
+ }
63
+ const debris = listDebrisAgentIdentities();
64
+ if (options.json) {
65
+ console.log(JSON.stringify({
66
+ debris: debris.map((d) => ({
67
+ agent_id: d.identity.agent_id,
68
+ agent_name: d.identity.agent_name,
69
+ trust_level: d.identity.trust_level,
70
+ reason: d.reason,
71
+ })),
72
+ }, null, 2));
73
+ return;
74
+ }
75
+ if (debris.length === 0) {
76
+ console.log('✔ No debris agent identities found.');
77
+ return;
78
+ }
79
+ console.log(`⚠ ${debris.length} debris agent identit${debris.length === 1 ? 'y' : 'ies'} found:`);
80
+ for (const d of debris) {
81
+ console.log(` - ${d.identity.agent_name} (${d.identity.agent_id}): ${d.reason}`);
82
+ }
83
+ console.log('Remove with `brainclaw register-agent <name> --remove`.');
84
+ }
29
85
  //# sourceMappingURL=register-agent.js.map
@@ -26,6 +26,7 @@
26
26
  import fs from 'node:fs';
27
27
  import path from 'node:path';
28
28
  import { memoryExists, memoryDir } from '../core/io.js';
29
+ import { migrateRuntimeNoteIdPrefixes } from '../core/runtime.js';
29
30
  import { runDoctor } from './doctor.js';
30
31
  /**
31
32
  * Execute a single repair candidate. Returns the outcome; never throws.
@@ -66,6 +67,25 @@ function executeCandidate(candidate, cwd) {
66
67
  fs.renameSync(sourceAbs, destAbs);
67
68
  return { action: candidate.action, target: candidate.target, status: 'applied' };
68
69
  }
70
+ case 'migrate_runtime_note_ids': {
71
+ // can_b8d53d18 — lossless rename of legacy run_-prefixed runtime note
72
+ // ids to rtn_ (file rename + id rewrite, no deletion of note content).
73
+ const migration = migrateRuntimeNoteIdPrefixes(cwd);
74
+ if (migration.errors.length > 0) {
75
+ return {
76
+ action: candidate.action,
77
+ target: candidate.target,
78
+ status: migration.migrated.length > 0 ? 'applied' : 'failed',
79
+ reason: `migrated ${migration.migrated.length}, errors: ${migration.errors.join('; ')}`,
80
+ };
81
+ }
82
+ return {
83
+ action: candidate.action,
84
+ target: candidate.target,
85
+ status: 'applied',
86
+ reason: `migrated ${migration.migrated.length} runtime note id(s) to rtn_`,
87
+ };
88
+ }
69
89
  case 'quarantine_inbox_message': {
70
90
  // Unsafe: move malformed message to a quarantine directory so a human
71
91
  // can inspect it. Never deletes. Requires includeUnsafe.
@@ -36,13 +36,15 @@ import { appendAuditEntry, readAuditLog } from '../core/audit.js';
36
36
  import { requireMinimumTrustLevel, requireRegisteredAgentIdentity } from '../core/agent-registry.js';
37
37
  import { loadSessionSnapshot } from '../commands/session-start.js';
38
38
  import { extractFilesFromDiff } from '../commands/handoff.js';
39
+ import { capHandoffDiff } from '../core/handoff-snapshot.js';
39
40
  import { suggestCompaction } from '../core/memory-compactor.js';
40
41
  import { dispatchReview } from '../core/dispatcher.js';
41
42
  export const REFLECTION_QUESTIONS = [
42
- 'What was the biggest time waste in this session, and how could it have been avoided?',
43
- 'What should have been done differently (design, process, or approach)?',
44
- 'What should brainclaw itself improve based on this session?',
43
+ 'Dogfooding the project using brainclaw to do real work this session, what friction did you hit (slow reads, confusing surfaces, missing affordances, awkward workflows)? What concrete change to the project would have removed it?',
44
+ 'Your surfaces, skills & tools did your generated surface files (CLAUDE.md / agent surface), skills (SKILL.md), or tools (MCP / CLI) help or get in the way? Name at least one concrete edit that would make them serve you better next time.',
45
+ 'What was the biggest time waste this session, and how could it have been avoided?',
45
46
  ];
47
+ export const REFLECTION_INSTRUCTION = 'Take a short reflection pass before you stop. For each question, capture anything ACTIONABLE as durable memory with bclaw_quick_capture (type "trap" for a sharp edge to avoid, "decision"/"note" for an improvement idea) so it enters the improvement backlog — improvements to the project AND to your own brainclaw surfaces/skills/tools both count. Use bclaw_write_note with tags ["reflection", "session:<id>"] for free-form narrative. Skipping is fine if the session was trivial.';
46
48
  export async function runSessionEnd(options = {}) {
47
49
  try {
48
50
  const result = await endSession(options);
@@ -362,10 +364,14 @@ export async function endSession(options = {}) {
362
364
  compaction_hint: compactionHint,
363
365
  ...(reflectedHandoff ? { handoff: reflectedHandoff } : {}),
364
366
  };
365
- if (options.reflect) {
367
+ // pln#564 — session_end pushes the agent into a short dogfooding reflection
368
+ // by DEFAULT (opt-out via reflect:false). The session_end runtime note is the
369
+ // natural trigger to ask "did the tooling serve me, and what should improve?"
370
+ // — both for the project worked on and for the agent's own brainclaw surfaces.
371
+ if (options.reflect !== false) {
366
372
  result.reflection_prompt = {
367
373
  questions: [...REFLECTION_QUESTIONS],
368
- instruction: `Please reflect on this session and answer each question. Write your answers using bclaw_write_note with tags ["reflection", "session:${sessionId}"]. One note per question, or a single combined note.`,
374
+ instruction: REFLECTION_INSTRUCTION.replace('session:<id>', `session:${sessionId}`),
369
375
  };
370
376
  }
371
377
  return result;
@@ -395,7 +401,10 @@ function materializeSessionHandoff(input) {
395
401
  linked_plans: input.linkedPlans.length > 0 ? input.linkedPlans : undefined,
396
402
  }
397
403
  : undefined,
398
- snapshot: input.fullDiff ? { diff: input.fullDiff } : undefined,
404
+ // pln#569 cap the inline diff to a preview + digest (the full ~450 KB
405
+ // uncommitted diff bloated auto-handoffs to 53 MB of the journal; no consumer
406
+ // reads past a bounded prefix and the worktree branch carries the full diff).
407
+ snapshot: capHandoffDiff(input.fullDiff),
399
408
  });
400
409
  persistState(state, cwd);
401
410
  return { handoff_id: id, plan_id: input.planId };
@@ -15,7 +15,8 @@ import { releaseStaleClaimsFromOtherAgents } from '../core/claims.js';
15
15
  import { SessionSnapshotSchema } from '../core/schema.js';
16
16
  import { auditLocalAgentWorkspaceFiles } from '../core/agent-files.js';
17
17
  import { buildAgentInventory, loadAgentInventory, saveAgentInventory, diffInventory } from '../core/agent-inventory.js';
18
- import { checkMemoryPressure } from '../core/gc-semantic.js';
18
+ import { checkMemoryPressure, enforceRuntimeNoteRetention } from '../core/gc-semantic.js';
19
+ import { maybeCreateCheckpoint } from '../core/events/checkpoint.js';
19
20
  import { pullSignalsFromLinkedProjects, markSignalProcessed } from '../core/federation-transport.js';
20
21
  import { pullSignalsFromCloud, isCloudSyncEnabled } from '../core/federation-cloud.js';
21
22
  import { materializeFederationSignal } from '../core/federation-materialize.js';
@@ -191,6 +192,22 @@ export async function startSession(options = {}) {
191
192
  inventoryAdvisory = lines;
192
193
  }
193
194
  catch { /* non-fatal — inventory scan failure should not block session start */ }
195
+ // pln#564 step B — cap the runtime-note tree on session start (no LLM gate,
196
+ // unlike the compaction-phase archiveSessionNotes). Keeps the newest N
197
+ // session/lifecycle notes per agent + all genuine observations, parks the
198
+ // rest. Bounds the buildContext read-path scan (trp_439fec51). Best-effort.
199
+ try {
200
+ enforceRuntimeNoteRetention({ cwd: options.cwd });
201
+ }
202
+ catch { /* non-fatal — retention sweep must never block session start */ }
203
+ // pln#566 Inc0 — keep a recent journal-derived checkpoint available off the
204
+ // hot path so the (capability-gated, OFF by default) checkpointRead read
205
+ // path has something to serve once enabled. Gated by a growth threshold so
206
+ // it only builds occasionally; journal-derived (F6). Best-effort.
207
+ try {
208
+ maybeCreateCheckpoint(options.cwd);
209
+ }
210
+ catch { /* non-fatal — checkpoint build must never block session start */ }
194
211
  }
195
212
  // Shared checkout detection: warn if other active sessions share the same worktree
196
213
  let sharedCheckoutWarning;
@@ -2,7 +2,14 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { memoryExists, memoryPath, ensureMemoryDir } from '../core/io.js';
4
4
  import { loadConfig, saveConfig } from '../core/config.js';
5
- import { generateBashGuard, generatePowerShellGuard, generatePipBashGuard } from '../core/security-guard.js';
5
+ import { generateBashGuard, generatePowerShellGuard, generatePipBashGuard, } from '../core/security-guard.js';
6
+ /**
7
+ * Each guard wraps a single install command. The CLI invocation that gets
8
+ * called is identical — only ORIGINAL_CMD differs — so we generate one
9
+ * script per supported tool with the right ORIGINAL_CMD baked in.
10
+ */
11
+ const NPM_LIKE = ['npm', 'pnpm', 'yarn'];
12
+ const PIP_LIKE = ['pip', 'pip3'];
6
13
  export function runSetupSecurity(options = {}) {
7
14
  const cwd = options.cwd;
8
15
  if (!memoryExists(cwd)) {
@@ -13,7 +20,16 @@ export function runSetupSecurity(options = {}) {
13
20
  const mode = options.mode ?? 'advisory';
14
21
  // Enable preinstall in config
15
22
  if (!config.security) {
16
- config.security = { mode: 'warn', strict_redaction: false, block_sensitive_paths: true };
23
+ config.security = {
24
+ mode: 'warn',
25
+ strict_redaction: false,
26
+ block_sensitive_paths: true,
27
+ token_detection: {
28
+ enabled: true,
29
+ entropy: { enabled: true, min_length: 32, min_entropy: 4.0 },
30
+ detectors: {},
31
+ },
32
+ };
17
33
  }
18
34
  config.security.preinstall = {
19
35
  enabled: true,
@@ -46,25 +62,30 @@ export function runSetupSecurity(options = {}) {
46
62
  if (!fs.existsSync(guardDir)) {
47
63
  fs.mkdirSync(guardDir, { recursive: true });
48
64
  }
49
- // npm guard (bash)
50
- const npmBashPath = path.join(guardDir, 'npm');
51
- fs.writeFileSync(npmBashPath, generateBashGuard(brainclawBin), { mode: 0o755 });
52
- // npm guard (PowerShell)
53
- const npmPs1Path = path.join(guardDir, 'npm.ps1');
54
- fs.writeFileSync(npmPs1Path, generatePowerShellGuard(brainclawBin));
55
- // pip guard (bash)
56
- const pipBashPath = path.join(guardDir, 'pip');
57
- fs.writeFileSync(pipBashPath, generatePipBashGuard(brainclawBin), { mode: 0o755 });
58
- // pip guard (PowerShell)
59
- const pipPs1Path = path.join(guardDir, 'pip.ps1');
60
- fs.writeFileSync(pipPs1Path, generatePowerShellGuard(brainclawBin).replace('} else { "npm" }', '} else { "pip" }'));
65
+ const writtenScripts = [];
66
+ for (const cmd of NPM_LIKE) {
67
+ const bashPath = path.join(guardDir, cmd);
68
+ const bashScript = generateBashGuard(brainclawBin).replace('ORIGINAL_CMD="${BRAINCLAW_GUARD_ORIGINAL_CMD:-npm}"', `ORIGINAL_CMD="\${BRAINCLAW_GUARD_ORIGINAL_CMD:-${cmd}}"`);
69
+ fs.writeFileSync(bashPath, bashScript, { mode: 0o755 });
70
+ writtenScripts.push(bashPath);
71
+ const ps1Path = path.join(guardDir, `${cmd}.ps1`);
72
+ const ps1Script = generatePowerShellGuard(brainclawBin).replace('} else { "npm" }', `} else { "${cmd}" }`);
73
+ fs.writeFileSync(ps1Path, ps1Script);
74
+ writtenScripts.push(ps1Path);
75
+ }
76
+ for (const cmd of PIP_LIKE) {
77
+ const bashPath = path.join(guardDir, cmd);
78
+ fs.writeFileSync(bashPath, generatePipBashGuard(brainclawBin).replace('ORIGINAL_CMD="${BRAINCLAW_GUARD_ORIGINAL_CMD:-pip}"', `ORIGINAL_CMD="\${BRAINCLAW_GUARD_ORIGINAL_CMD:-${cmd}}"`), { mode: 0o755 });
79
+ writtenScripts.push(bashPath);
80
+ const ps1Path = path.join(guardDir, `${cmd}.ps1`);
81
+ fs.writeFileSync(ps1Path, generatePowerShellGuard(brainclawBin).replace('} else { "npm" }', `} else { "${cmd}" }`));
82
+ writtenScripts.push(ps1Path);
83
+ }
61
84
  console.log(`\u2705 Security gate enabled (mode: ${mode})`);
62
85
  console.log('');
63
86
  console.log('Generated wrapper scripts:');
64
- console.log(` ${npmBashPath}`);
65
- console.log(` ${npmPs1Path}`);
66
- console.log(` ${pipBashPath}`);
67
- console.log(` ${pipPs1Path}`);
87
+ for (const p of writtenScripts)
88
+ console.log(` ${p}`);
68
89
  console.log('');
69
90
  console.log('To activate, prepend the guard directory to your PATH:');
70
91
  console.log(` export PATH="${guardDir}:$PATH" # bash/zsh`);