brainclaw 1.24.0 → 1.26.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 (46) hide show
  1. package/dist/brainclaw-vscode.vsix +0 -0
  2. package/dist/cli/register-code-map.js +9 -2
  3. package/dist/commands/code-map.js +120 -6
  4. package/dist/commands/mcp-catalog.js +46 -0
  5. package/dist/commands/mcp.js +58 -6
  6. package/dist/commands/session-start.js +84 -13
  7. package/dist/core/bootstrap.js +28 -4
  8. package/dist/core/code-map/aggregate.js +36 -31
  9. package/dist/core/code-map/backend.js +162 -5
  10. package/dist/core/code-map/core.js +1 -0
  11. package/dist/core/code-map/export.js +212 -0
  12. package/dist/core/code-map/finalizer.js +57 -2
  13. package/dist/core/code-map/freshness.js +81 -15
  14. package/dist/core/code-map/impact.js +409 -0
  15. package/dist/core/code-map/indexes.js +64 -3
  16. package/dist/core/code-map/lang/python/index.js +4 -2
  17. package/dist/core/code-map/lang/query-runtime.js +2 -0
  18. package/dist/core/code-map/lang/typescript/config.js +271 -0
  19. package/dist/core/code-map/lang/typescript/index.js +24 -6
  20. package/dist/core/code-map/lang/usages.js +333 -0
  21. package/dist/core/code-map/memory-reader.js +15 -0
  22. package/dist/core/code-map/query.js +285 -71
  23. package/dist/core/code-map/refresh.js +0 -0
  24. package/dist/core/code-map/resolve.js +28 -2
  25. package/dist/core/code-map/store.js +1 -0
  26. package/dist/core/code-map/types.js +70 -9
  27. package/dist/core/code-map/vocabulary.js +6 -0
  28. package/dist/core/code-map/work-section.js +12 -14
  29. package/dist/core/context-diff.js +17 -3
  30. package/dist/core/entity-operations.js +14 -2
  31. package/dist/core/federation-pull.js +151 -3
  32. package/dist/core/federation-push.js +16 -3
  33. package/dist/core/hint-aging.js +4 -1
  34. package/dist/core/identity.js +69 -17
  35. package/dist/core/io.js +27 -0
  36. package/dist/core/project-discovery.js +7 -1
  37. package/dist/core/protocol-tool-policy.js +3 -0
  38. package/dist/core/runtime.js +23 -0
  39. package/dist/core/worktree.js +89 -2
  40. package/dist/facts.js +15 -12
  41. package/dist/facts.json +14 -11
  42. package/docs/cli.md +8 -0
  43. package/docs/code-map.md +60 -28
  44. package/docs/integrations/mcp.md +5 -2
  45. package/docs/mcp-schema-changelog.md +11 -1
  46. package/package.json +1 -1
@@ -6,7 +6,7 @@ import { detectAiAgent } from './ai-agent-detection.js';
6
6
  import { requireRegisteredAgentIdentity } from './agent-registry.js';
7
7
  import { loadConfig } from './config.js';
8
8
  import { resolveCurrentHostId } from './host.js';
9
- import { memoryDir } from './io.js';
9
+ import { isSessionSnapshotRecordFilename, memoryDir } from './io.js';
10
10
  import { loadVersionedJsonFile, saveVersionedJsonFile } from './migration.js';
11
11
  import { CurrentSessionStateSchema } from './schema.js';
12
12
  const SESSIONS_DIR = 'sessions';
@@ -87,7 +87,7 @@ export function loadCurrentSession(cwd) {
87
87
  // Multiple parallel agents can have the same agent name/user in one repo;
88
88
  // a live different PID is a different agent instance, not our session.
89
89
  if (fs.existsSync(dir) && currentAgent) {
90
- const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
90
+ const files = listCurrentSessionFiles(dir);
91
91
  const legacyPidlessCandidates = [];
92
92
  for (const file of files) {
93
93
  try {
@@ -133,15 +133,25 @@ export function loadCurrentSession(cwd) {
133
133
  * Load a specific session by ID.
134
134
  */
135
135
  export function loadSessionById(sessionId, cwd) {
136
- const filepath = sessionFilePath(sessionId, cwd);
136
+ let filepath;
137
+ try {
138
+ filepath = sessionFilePath(sessionId, cwd);
139
+ }
140
+ catch {
141
+ // Reserved '.snapshot' alias id — such a current_session record can never exist.
142
+ return undefined;
143
+ }
137
144
  if (!fs.existsSync(filepath))
138
145
  return undefined;
139
146
  try {
140
147
  const migration = loadVersionedJsonFile('current_session', filepath);
141
- return {
148
+ const session = {
142
149
  ...CurrentSessionStateSchema.parse(migration.document),
143
150
  schema_version: migration.metadata.currentVersion,
144
151
  };
152
+ // The filename is only an index, never an identity (codex review): do not
153
+ // adopt a record whose payload names a different session.
154
+ return session.session_id === sessionId ? session : undefined;
145
155
  }
146
156
  catch {
147
157
  return undefined;
@@ -154,7 +164,7 @@ export function loadAllSessions(cwd) {
154
164
  const dir = sessionsDir(cwd);
155
165
  if (!fs.existsSync(dir))
156
166
  return [];
157
- const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
167
+ const files = listCurrentSessionFiles(dir);
158
168
  const sessions = [];
159
169
  for (const file of files) {
160
170
  try {
@@ -178,7 +188,15 @@ export function saveCurrentSession(session, cwd) {
178
188
  if (!fs.existsSync(dir)) {
179
189
  fs.mkdirSync(dir, { recursive: true });
180
190
  }
191
+ // sessionFilePath throws on a '.snapshot' alias id — a write must never
192
+ // construct a snapshot filename (codex review P1).
181
193
  const filepath = sessionFilePath(session.session_id, cwd);
194
+ // A plain legacy `<id>.json` can still hold a pre-split snapshot (the old
195
+ // 'read'-mode write bug parked snapshots in this directory). Only overwrite
196
+ // a record that PROVES it is this exact current_session entry (codex review).
197
+ if (fs.existsSync(filepath) && !loadSessionById(session.session_id, cwd)) {
198
+ throw new Error(`Refusing to overwrite non-current_session record at '${filepath}'`);
199
+ }
182
200
  saveVersionedJsonFile('current_session', filepath, CurrentSessionStateSchema.parse(session));
183
201
  }
184
202
  /**
@@ -186,10 +204,15 @@ export function saveCurrentSession(session, cwd) {
186
204
  */
187
205
  export function clearCurrentSession(cwd, sessionId) {
188
206
  if (sessionId) {
189
- // Remove specific session file
190
- const filepath = sessionFilePath(sessionId, cwd);
207
+ // Remove specific session file. A filename is never enough authority to
208
+ // delete a record (codex review P1): the '.snapshot' alias id throws in
209
+ // sessionFilePath (caught → no-op), and an existing file is only unlinked
210
+ // when loadSessionById PROVES it is this exact current_session record —
211
+ // never a pre-split snapshot parked under a plain `<id>.json`.
191
212
  try {
192
- fs.unlinkSync(filepath);
213
+ if (loadSessionById(sessionId, cwd)) {
214
+ fs.unlinkSync(sessionFilePath(sessionId, cwd));
215
+ }
193
216
  }
194
217
  catch { /* ignore */ }
195
218
  return;
@@ -221,26 +244,30 @@ export function gcStaleSessions(cwd, ttlOverride) {
221
244
  const ttlMs = parseDurationToMs(ttlOverride ?? loadConfigSafe(cwd)?.implicit_session_ttl ?? '4h');
222
245
  const now = Date.now();
223
246
  let removed = 0;
224
- const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
247
+ const files = listCurrentSessionFiles(dir);
225
248
  for (const file of files) {
249
+ const filepath = path.join(dir, file);
226
250
  try {
227
- const migration = loadVersionedJsonFile('current_session', path.join(dir, file));
251
+ // POSITIVE proof before deletion (codex review): a bare `<id>.json` in
252
+ // this directory can still be a pre-split snapshot (old 'read'-mode
253
+ // write bug). Only a record carrying the current_session discriminant
254
+ // may be collected; anything unidentifiable is preserved, never deleted.
255
+ const raw = JSON.parse(fs.readFileSync(filepath, 'utf-8'));
256
+ if (typeof raw.last_seen_at !== 'string')
257
+ continue;
258
+ const migration = loadVersionedJsonFile('current_session', filepath);
228
259
  const session = {
229
260
  ...CurrentSessionStateSchema.parse(migration.document),
230
261
  schema_version: migration.metadata.currentVersion,
231
262
  };
232
263
  if (now - Date.parse(session.last_seen_at) > ttlMs) {
233
- fs.unlinkSync(path.join(dir, file));
264
+ fs.unlinkSync(filepath);
234
265
  removed++;
235
266
  }
236
267
  }
237
268
  catch {
238
- // Remove unparseable files too
239
- try {
240
- fs.unlinkSync(path.join(dir, file));
241
- removed++;
242
- }
243
- catch { /* ignore */ }
269
+ // An unidentifiable record is not proven stale current_session state —
270
+ // preserving it beats risking the deletion of a legacy snapshot.
244
271
  }
245
272
  }
246
273
  return removed;
@@ -249,7 +276,32 @@ export function gcStaleSessions(cwd, ttlOverride) {
249
276
  function sessionsDir(cwd) {
250
277
  return path.join(memoryDir(cwd), SESSIONS_DIR);
251
278
  }
279
+ /**
280
+ * pln#670 — current_session scanners must be type-strict: session_snapshot
281
+ * records use the `<id>.snapshot.json` suffix and can share a directory with
282
+ * current_session records. Without this exclusion, gcStaleSessions would
283
+ * delete a stray snapshot as "unparseable" and loadCurrentSession could adopt
284
+ * one as a session candidate.
285
+ */
286
+ function listCurrentSessionFiles(dir) {
287
+ // Case-insensitive on purpose (codex review P1): Windows filesystems match
288
+ // names case-insensitively, so `X.SNAPSHOT.json` IS the snapshot path a
289
+ // lower-case probe resolves — excluding only the exact-case suffix would let
290
+ // gcStaleSessions delete it as an unparseable current_session.
291
+ return fs.readdirSync(dir).filter(f => f.toLowerCase().endsWith('.json') && !isSessionSnapshotRecordFilename(f));
292
+ }
293
+ /** True when `<sessionId>.json` cannot collide with a session_snapshot filename. */
294
+ function isCurrentSessionFilename(sessionId) {
295
+ return !isSessionSnapshotRecordFilename(`${sessionId}.json`);
296
+ }
252
297
  function sessionFilePath(sessionId, cwd) {
298
+ // pln#670 review fix (codex P1): a session id ending in ".snapshot" would
299
+ // produce `<base>.snapshot.json` — the snapshot filename of session <base>
300
+ // in a shared directory. Refuse the alias instead of silently colliding
301
+ // across record types; readers treat the throw as "no such record".
302
+ if (!isCurrentSessionFilename(sessionId)) {
303
+ throw new Error(`session id '${sessionId}' is reserved for session_snapshot records — the '.snapshot' suffix would collide across record types`);
304
+ }
253
305
  return path.join(sessionsDir(cwd), `${sessionId}.json`);
254
306
  }
255
307
  function resolveCurrentUser() {
package/dist/core/io.js CHANGED
@@ -121,6 +121,33 @@ export function entityRecordDirs(subdir, cwd = process.cwd(), preferredDirName)
121
121
  export function entityRecordPaths(subdir, id, cwd, preferredDirName) {
122
122
  return entityRecordDirs(subdir, cwd ?? process.cwd(), preferredDirName).map((d) => path.join(d, `${id}.json`));
123
123
  }
124
+ export const SESSION_SNAPSHOT_FILENAME_SUFFIX = '.snapshot.json';
125
+ /**
126
+ * Filesystem type discriminator for session snapshots (codex review, pln#670).
127
+ * Case-fold before the suffix comparison because default Windows filesystems
128
+ * are case-insensitive: `X.SNAPSHOT.json` IS the path a lower-case probe
129
+ * resolves, and every suffix decision must agree on its type.
130
+ */
131
+ export function isSessionSnapshotRecordFilename(filename) {
132
+ return filename.toLowerCase().endsWith(SESSION_SNAPSHOT_FILENAME_SUFFIX);
133
+ }
134
+ /**
135
+ * EVERY path a session_snapshot record for `sessionId` can occupy, canonical first.
136
+ *
137
+ * session_snapshot and current_session are two different record types that share
138
+ * the `sessions` directory family AND the same session_id — only the filename keeps
139
+ * them apart (pln#670). Snapshots are written as `<id>.snapshot.json` so a
140
+ * current_session `<id>.json` for the same session can never clobber them, whatever
141
+ * directory each resolver picks. The plain `<id>.json` probes cover records written
142
+ * before the split; readers must schema-validate every candidate.
143
+ */
144
+ export function sessionSnapshotRecordPaths(sessionId, cwd, preferredDirName) {
145
+ const dirs = entityRecordDirs('sessions', cwd ?? process.cwd(), preferredDirName);
146
+ return [
147
+ ...dirs.map((d) => path.join(d, `${sessionId}.snapshot.json`)),
148
+ ...dirs.map((d) => path.join(d, `${sessionId}.json`)),
149
+ ];
150
+ }
124
151
  export function memoryDir(cwd = process.cwd(), preferredDirName) {
125
152
  return path.join(cwd, preferredDirName ?? MEMORY_DIR);
126
153
  }
@@ -222,7 +222,13 @@ function discoverFiles(cwd, files, dirs) {
222
222
  }
223
223
  return results;
224
224
  }
225
- function isManagedByBrainclaw(filePath) {
225
+ /**
226
+ * True when an instruction file was generated by `brainclaw export` (or carries
227
+ * a managed section marker). Exported for the bootstrap scan (pln#671): a
228
+ * managed export derives FROM brainclaw memory, so proposing it as a bootstrap
229
+ * import would feed brainclaw its own output back as new knowledge.
230
+ */
231
+ export function isManagedByBrainclaw(filePath) {
226
232
  try {
227
233
  const content = fs.readFileSync(filePath, 'utf-8').slice(0, 200);
228
234
  return content.includes('brainclaw') && (content.includes('Managed by brainclaw') ||
@@ -48,6 +48,9 @@ export const MCP_HEADLESS_AUTO_TOOL_NAMES = [
48
48
  'bclaw_code_status',
49
49
  'bclaw_code_find',
50
50
  'bclaw_code_brief',
51
+ 'bclaw_code_impact',
52
+ 'bclaw_code_export',
53
+ 'bclaw_code_outline',
51
54
  'bclaw_send_message',
52
55
  'bclaw_ack_message',
53
56
  'bclaw_write_note',
@@ -68,6 +68,29 @@ export function runtimeNotePath(note, cwd) {
68
68
  ? path.join(sharedAgentDir(note.agent, cwd), `${note.id}.json`)
69
69
  : path.join(hostAgentDir(visibility, hostId, note.agent, cwd), `${note.id}.json`);
70
70
  }
71
+ /**
72
+ * Park one runtime note's raw record under `.brainclaw/gc-backups/` — the same
73
+ * park-don't-delete net the retention sweeps use (trp_dc9ca61e). Daily-bucketed
74
+ * JSONL so removals do not explode into one file per note. Returns the backup
75
+ * path, or undefined when the source record cannot be read.
76
+ */
77
+ export function parkRuntimeNoteBackup(note, cwd) {
78
+ try {
79
+ const sourcePath = runtimeNotePath(note, cwd);
80
+ const content = fs.readFileSync(sourcePath, 'utf-8');
81
+ const parsed = JSON.parse(content);
82
+ parsed._removed_at = new Date().toISOString();
83
+ parsed._removal_type = 'bclaw_remove';
84
+ const day = new Date().toISOString().slice(0, 10);
85
+ const backupPath = path.join(cwd ?? process.cwd(), '.brainclaw', 'gc-backups', `removed-runtime-notes-${day}.jsonl`);
86
+ fs.mkdirSync(path.dirname(backupPath), { recursive: true });
87
+ fs.appendFileSync(backupPath, JSON.stringify(parsed) + '\n', 'utf-8');
88
+ return backupPath;
89
+ }
90
+ catch {
91
+ return undefined;
92
+ }
93
+ }
71
94
  export function deleteRuntimeNote(note, cwd) {
72
95
  return mutate({ cwd }, () => {
73
96
  const filepath = runtimeNotePath(note, cwd);
@@ -7,6 +7,7 @@ import yaml from 'yaml';
7
7
  import { logger } from './logger.js';
8
8
  import { loadConfig } from './config.js';
9
9
  import { parsePorcelainZ, isSystemDirtyPath } from './dirty-scope.js';
10
+ import { entityRecordDirs } from './io.js';
10
11
  /** Normalizes a path for use in git CLI arguments (forward slashes on Windows). */
11
12
  function gitPath(p) {
12
13
  return p.replace(/\\/g, '/');
@@ -1548,6 +1549,68 @@ export function probeLocalBranch(mainWorktreePath, branchName) {
1548
1549
  export function isGitRepo(cwd) {
1549
1550
  return runGit(['rev-parse', '--is-inside-work-tree'], cwd).ok;
1550
1551
  }
1552
+ /**
1553
+ * Comparison key for worktree paths: PHYSICAL identity when the path exists
1554
+ * (realpath expands Windows 8.3 short names — `RUNNER~1` and `runneradmin`
1555
+ * are the same directory but different strings, and git always reports the
1556
+ * long form while a claim may carry the short one), else plain resolution.
1557
+ * Forward slashes, case-folded on win32.
1558
+ */
1559
+ function worktreePathKey(p) {
1560
+ let resolved;
1561
+ try {
1562
+ resolved = fs.realpathSync.native(p);
1563
+ }
1564
+ catch {
1565
+ resolved = path.resolve(p);
1566
+ }
1567
+ resolved = resolved.replace(/\\/g, '/').replace(/\/+$/, '');
1568
+ return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
1569
+ }
1570
+ /**
1571
+ * Worktree paths referenced by an ACTIVE, non-expired claim — the set the GC
1572
+ * must never touch.
1573
+ *
1574
+ * Read directly from the claims record dirs (both layouts, pln#649) instead of
1575
+ * claims.ts: claims.ts imports worktree.ts, so the dependency can only point
1576
+ * this way. The parse is deliberately lenient — an unreadable claim simply does
1577
+ * not protect anything; it never blocks the GC of OTHER worktrees.
1578
+ *
1579
+ * Scope note: dispatch claims are project-local, so the project store is the
1580
+ * right authority here; workspace-level claims (cross-project) never carry a
1581
+ * lane worktree_path.
1582
+ */
1583
+ function activeClaimWorktreePaths(cwd) {
1584
+ const out = new Set();
1585
+ const now = new Date();
1586
+ for (const dir of entityRecordDirs('claims', cwd)) {
1587
+ let files;
1588
+ try {
1589
+ files = fs.readdirSync(dir).filter((f) => f.endsWith('.json'));
1590
+ }
1591
+ catch {
1592
+ continue;
1593
+ }
1594
+ for (const f of files) {
1595
+ try {
1596
+ const claim = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf-8'));
1597
+ if (claim.status !== 'active')
1598
+ continue;
1599
+ if (typeof claim.worktree_path !== 'string' || !claim.worktree_path)
1600
+ continue;
1601
+ // Mirror isClaimExpired: a zombie claim past its expiry must not make a
1602
+ // worktree un-GC-able forever.
1603
+ if (claim.expires_at && new Date(claim.expires_at) < now)
1604
+ continue;
1605
+ out.add(worktreePathKey(claim.worktree_path));
1606
+ }
1607
+ catch {
1608
+ /* lenient by design — see above */
1609
+ }
1610
+ }
1611
+ }
1612
+ return out;
1613
+ }
1551
1614
  /**
1552
1615
  * Removes worktrees whose branch has been fully merged into the current branch
1553
1616
  * (typically master/main after a merge). Also removes brainclaw-managed
@@ -1561,6 +1624,16 @@ export function isGitRepo(cwd) {
1561
1624
  * - content (`git cherry HEAD <branch>`, patch-id): catches squash merges,
1562
1625
  * which is GitHub's default merge strategy on this repo and previously left
1563
1626
  * every squashed lane un-GC-able forever.
1627
+ *
1628
+ * ACTIVE-CLAIM GATE (incident 2026-08-10): a freshly-dispatched lane worktree
1629
+ * has no commits of its own — its branch IS an ancestor of HEAD, so both merged
1630
+ * probes say "merged" — and before the agent's first write it has no uncommitted
1631
+ * changes either. Both historical gates therefore pass during a lane's startup
1632
+ * window, and the post-merge hook destroyed a live codex lane 7 minutes after
1633
+ * spawn (worktree emptied under the running agent). The coordination store is
1634
+ * the authority on liveness: a worktree referenced by an active claim is
1635
+ * untouchable — merged or not, clean or not, force or not. The escape hatch is
1636
+ * releasing the claim, never bypassing it.
1564
1637
  */
1565
1638
  export function cleanMergedWorktrees(mainWorktreePath, options = {}) {
1566
1639
  const result = { removed: [], skipped: [], pruned: false };
@@ -1580,9 +1653,16 @@ export function cleanMergedWorktrees(mainWorktreePath, options = {}) {
1580
1653
  .filter(Boolean)
1581
1654
  : []);
1582
1655
  const worktrees = listWorktrees(mainWorktreePath);
1656
+ const protectedPaths = activeClaimWorktreePaths(mainWorktreePath);
1583
1657
  for (const wt of worktrees) {
1584
1658
  if (wt.is_main)
1585
1659
  continue;
1660
+ // Active-claim gate — see the function doc. Checked BEFORE the merged
1661
+ // probes and BEFORE `force`: a live dispatched lane is never GC-able.
1662
+ if (protectedPaths.has(worktreePathKey(wt.path))) {
1663
+ result.skipped.push({ path: wt.path, reason: 'active claim' });
1664
+ continue;
1665
+ }
1586
1666
  // trp#926 — a lane's branch is "merged" if EITHER git says its commits are
1587
1667
  // ancestors of HEAD (fast-forward / merge-commit) OR every commit's patch
1588
1668
  // is already on HEAD (squash-merge, catching GitHub's default strategy).
@@ -1621,7 +1701,7 @@ export function cleanMergedWorktrees(mainWorktreePath, options = {}) {
1621
1701
  }
1622
1702
  }
1623
1703
  // Clean orphan brainclaw worktree directories (no matching git worktree)
1624
- cleanOrphanWorktreeDirs(mainWorktreePath, worktrees, result, options.dryRun);
1704
+ cleanOrphanWorktreeDirs(mainWorktreePath, worktrees, result, options.dryRun, protectedPaths);
1625
1705
  return result;
1626
1706
  }
1627
1707
  /** A worker whose heartbeat file was touched within this window looks alive. */
@@ -1722,7 +1802,7 @@ export function gcWorktreeIfHarvested(mainWorktreePath, worktreePath, options =
1722
1802
  * Removes brainclaw-managed worktree directories under ~/.brainclaw/worktrees/
1723
1803
  * that no longer have a corresponding git worktree entry.
1724
1804
  */
1725
- function cleanOrphanWorktreeDirs(mainWorktreePath, activeWorktrees, result, dryRun) {
1805
+ function cleanOrphanWorktreeDirs(mainWorktreePath, activeWorktrees, result, dryRun, protectedPaths = new Set()) {
1726
1806
  const base = worktreesBaseDir(mainWorktreePath);
1727
1807
  if (!fs.existsSync(base))
1728
1808
  return;
@@ -1740,6 +1820,13 @@ function cleanOrphanWorktreeDirs(mainWorktreePath, activeWorktrees, result, dryR
1740
1820
  const dirPath = path.resolve(path.join(base, entry.name));
1741
1821
  if (activePaths.has(dirPath))
1742
1822
  continue;
1823
+ // Active-claim gate: a dir whose git admin entry vanished can still host a
1824
+ // LIVE agent (the 2026-08-10 incident left exactly this state behind). If a
1825
+ // claim still points here, it is not debris.
1826
+ if (protectedPaths.has(worktreePathKey(dirPath))) {
1827
+ result.skipped.push({ path: dirPath, reason: 'active claim' });
1828
+ continue;
1829
+ }
1743
1830
  // This directory is not referenced by any git worktree — it's orphaned
1744
1831
  if (dryRun) {
1745
1832
  result.removed.push(dirPath);
package/dist/facts.js CHANGED
@@ -1,11 +1,11 @@
1
1
  // Generated by scripts/emit-site-facts.mjs at build time. Do not edit manually.
2
- // Source: brainclaw v1.24.0 on 2026-08-10T18:03:29.240Z
2
+ // Source: brainclaw v1.26.0 on 2026-08-16T20:27:49.404Z
3
3
  export const FACTS = {
4
- "version": "1.24.0",
5
- "generated_at": "2026-08-10T18:03:29.240Z",
4
+ "version": "1.26.0",
5
+ "generated_at": "2026-08-16T20:27:49.404Z",
6
6
  "tools": {
7
- "count": 67,
8
- "published_count": 65,
7
+ "count": 70,
8
+ "published_count": 68,
9
9
  "names": [
10
10
  "bclaw_bootstrap",
11
11
  "bclaw_release_notes",
@@ -34,6 +34,9 @@ export const FACTS = {
34
34
  "bclaw_code_status",
35
35
  "bclaw_code_find",
36
36
  "bclaw_code_brief",
37
+ "bclaw_code_impact",
38
+ "bclaw_code_export",
39
+ "bclaw_code_outline",
37
40
  "bclaw_code_refresh",
38
41
  "bclaw_dispatch",
39
42
  "bclaw_send_message",
@@ -474,8 +477,8 @@ export const FACTS = {
474
477
  },
475
478
  "bench": {
476
479
  "schema": "brainclaw.bench.v1",
477
- "generated_at": "2026-08-10T18:03:27.087Z",
478
- "node_version": "v24.18.0",
480
+ "generated_at": "2026-08-16T20:27:47.271Z",
481
+ "node_version": "v24.19.0",
479
482
  "platform": "linux-x64",
480
483
  "repeats": 3,
481
484
  "scenarios": [
@@ -483,7 +486,7 @@ export const FACTS = {
483
486
  "name": "cold_onboard",
484
487
  "volume": "empty",
485
488
  "description": "fresh machine → init → first useful context. Baseline for time-to-first-value.",
486
- "duration_ms_median": 71,
489
+ "duration_ms_median": 75,
487
490
  "payload_chars_median": 1640,
488
491
  "payload_tokens_est_median": 410
489
492
  },
@@ -491,7 +494,7 @@ export const FACTS = {
491
494
  "name": "warm_work",
492
495
  "volume": "medium",
493
496
  "description": "bclaw_work consult over a real-shaped store (~200 plans / 500 handoffs / 450 claims).",
494
- "duration_ms_median": 107,
497
+ "duration_ms_median": 132,
495
498
  "payload_chars_median": 2626,
496
499
  "payload_tokens_est_median": 657
497
500
  },
@@ -499,9 +502,9 @@ export const FACTS = {
499
502
  "name": "first_edit",
500
503
  "volume": "medium",
501
504
  "description": "code_find + code_brief on the fresh-agent path (missing index, first touch).",
502
- "duration_ms_median": 10,
503
- "payload_chars_median": 499,
504
- "payload_tokens_est_median": 125
505
+ "duration_ms_median": 11,
506
+ "payload_chars_median": 1305,
507
+ "payload_tokens_est_median": 326
505
508
  }
506
509
  ]
507
510
  }
package/dist/facts.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "1.24.0",
3
- "generated_at": "2026-08-10T18:03:29.240Z",
2
+ "version": "1.26.0",
3
+ "generated_at": "2026-08-16T20:27:49.404Z",
4
4
  "tools": {
5
- "count": 67,
6
- "published_count": 65,
5
+ "count": 70,
6
+ "published_count": 68,
7
7
  "names": [
8
8
  "bclaw_bootstrap",
9
9
  "bclaw_release_notes",
@@ -32,6 +32,9 @@
32
32
  "bclaw_code_status",
33
33
  "bclaw_code_find",
34
34
  "bclaw_code_brief",
35
+ "bclaw_code_impact",
36
+ "bclaw_code_export",
37
+ "bclaw_code_outline",
35
38
  "bclaw_code_refresh",
36
39
  "bclaw_dispatch",
37
40
  "bclaw_send_message",
@@ -472,8 +475,8 @@
472
475
  },
473
476
  "bench": {
474
477
  "schema": "brainclaw.bench.v1",
475
- "generated_at": "2026-08-10T18:03:27.087Z",
476
- "node_version": "v24.18.0",
478
+ "generated_at": "2026-08-16T20:27:47.271Z",
479
+ "node_version": "v24.19.0",
477
480
  "platform": "linux-x64",
478
481
  "repeats": 3,
479
482
  "scenarios": [
@@ -481,7 +484,7 @@
481
484
  "name": "cold_onboard",
482
485
  "volume": "empty",
483
486
  "description": "fresh machine → init → first useful context. Baseline for time-to-first-value.",
484
- "duration_ms_median": 71,
487
+ "duration_ms_median": 75,
485
488
  "payload_chars_median": 1640,
486
489
  "payload_tokens_est_median": 410
487
490
  },
@@ -489,7 +492,7 @@
489
492
  "name": "warm_work",
490
493
  "volume": "medium",
491
494
  "description": "bclaw_work consult over a real-shaped store (~200 plans / 500 handoffs / 450 claims).",
492
- "duration_ms_median": 107,
495
+ "duration_ms_median": 132,
493
496
  "payload_chars_median": 2626,
494
497
  "payload_tokens_est_median": 657
495
498
  },
@@ -497,9 +500,9 @@
497
500
  "name": "first_edit",
498
501
  "volume": "medium",
499
502
  "description": "code_find + code_brief on the fresh-agent path (missing index, first touch).",
500
- "duration_ms_median": 10,
501
- "payload_chars_median": 499,
502
- "payload_tokens_est_median": 125
503
+ "duration_ms_median": 11,
504
+ "payload_chars_median": 1305,
505
+ "payload_tokens_est_median": 326
503
506
  }
504
507
  ]
505
508
  }
package/docs/cli.md CHANGED
@@ -652,6 +652,14 @@ Search the symbol index by name (function / class / component / hook / type). Re
652
652
  ### `brainclaw code-map brief <target> [--limit <n>]`
653
653
 
654
654
  Given a symbol or path, return a ranked reading list (`suggested_files_to_read`) plus related memory (decisions/traps/constraints) — what to read before editing.
655
+ ### `brainclaw code-map export <symbol-or-path> [--direction outgoing|incoming|both]`
656
+
657
+ Export a compact **local** Code Map subgraph around one symbol or file. The default
658
+ is one hop in both directions; hard caps always apply (depth 4, 100 nodes, 200
659
+ edges), so the command never defaults to a whole-graph export. `--max-nodes`,
660
+ `--max-edges`, and `--depth` only tighten the result; `--min-confidence` has a
661
+ hard floor of 0.5. JSON keeps each edge's `kind`, `source`, and `confidence`.
662
+ Use `--format mermaid` for a diagram projected from that same JSON model.
655
663
 
656
664
  ```bash
657
665
  brainclaw code-map refresh --all