brainclaw 1.25.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.
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/commands/code-map.js +1 -4
- package/dist/commands/mcp.js +7 -7
- package/dist/commands/session-start.js +84 -13
- package/dist/core/bootstrap.js +28 -4
- package/dist/core/code-map/aggregate.js +36 -31
- package/dist/core/code-map/backend.js +4 -4
- package/dist/core/code-map/core.js +1 -0
- package/dist/core/code-map/export.js +4 -4
- package/dist/core/code-map/finalizer.js +57 -2
- package/dist/core/code-map/freshness.js +78 -13
- package/dist/core/code-map/impact.js +36 -4
- package/dist/core/code-map/indexes.js +37 -0
- package/dist/core/code-map/lang/python/index.js +4 -2
- package/dist/core/code-map/lang/query-runtime.js +2 -0
- package/dist/core/code-map/lang/typescript/index.js +4 -2
- package/dist/core/code-map/lang/usages.js +333 -0
- package/dist/core/code-map/memory-reader.js +15 -0
- package/dist/core/code-map/query.js +209 -58
- package/dist/core/code-map/refresh.js +0 -0
- package/dist/core/code-map/resolve.js +27 -2
- package/dist/core/code-map/store.js +1 -0
- package/dist/core/code-map/types.js +55 -9
- package/dist/core/code-map/vocabulary.js +6 -0
- package/dist/core/code-map/work-section.js +12 -14
- package/dist/core/context-diff.js +17 -3
- package/dist/core/entity-operations.js +14 -2
- package/dist/core/hint-aging.js +4 -1
- package/dist/core/identity.js +69 -17
- package/dist/core/io.js +27 -0
- package/dist/core/project-discovery.js +7 -1
- package/dist/core/runtime.js +23 -0
- package/dist/facts.js +12 -12
- package/dist/facts.json +11 -11
- package/docs/code-map.md +36 -27
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { readAuditLog } from './audit.js';
|
|
3
3
|
import { listCandidates } from './candidates.js';
|
|
4
|
-
import {
|
|
4
|
+
import { sessionSnapshotRecordPaths } from './io.js';
|
|
5
5
|
import { loadVersionedJsonFile } from './migration.js';
|
|
6
6
|
import { buildNotificationSummary, hasEventCursor, readUnseenEvents, seedCursorToEnd } from './event-log.js';
|
|
7
7
|
import { SessionSnapshotSchema } from './schema.js';
|
|
@@ -44,11 +44,25 @@ export function resolveContextDiffSince(options) {
|
|
|
44
44
|
* Uses the shared primitive rather than a fourth hand-rolled pair of paths (io.ts).
|
|
45
45
|
*/
|
|
46
46
|
function loadSessionSnapshot(sessionId, cwd) {
|
|
47
|
-
|
|
47
|
+
// pln#670 — snapshots carry a type-suffixed name (`<id>.snapshot.json`);
|
|
48
|
+
// the helper also probes the pre-split `<id>.json` layouts.
|
|
49
|
+
for (const snapshotPath of sessionSnapshotRecordPaths(sessionId, cwd ?? process.cwd())) {
|
|
48
50
|
if (!fs.existsSync(snapshotPath))
|
|
49
51
|
continue;
|
|
50
52
|
try {
|
|
51
|
-
|
|
53
|
+
// Deliberately NOT type-strict (pln#649 + pln#670): this reader answers
|
|
54
|
+
// "when did session X start" to anchor the diff window, and a
|
|
55
|
+
// current_session record for the same id is an equally authoritative
|
|
56
|
+
// source of `started_at`. The suffixed probes come first, so a real
|
|
57
|
+
// snapshot (richer: context_target, git_sha) still wins when both exist.
|
|
58
|
+
// Contrast with session-start's loadSessionSnapshot, which must stay
|
|
59
|
+
// strict — its consumers treat the record as a genuine snapshot.
|
|
60
|
+
const snapshot = SessionSnapshotSchema.parse(loadVersionedJsonFile('session_snapshot', snapshotPath).document);
|
|
61
|
+
// A suffix-bearing lookup can construct another snapshot's path (codex
|
|
62
|
+
// review). Its started_at is useful only when the stored identity matches.
|
|
63
|
+
if (snapshot.session_id !== sessionId)
|
|
64
|
+
continue;
|
|
65
|
+
return snapshot;
|
|
52
66
|
}
|
|
53
67
|
catch {
|
|
54
68
|
// An unparseable record in one layout must not mask a good one in the other.
|
|
@@ -28,7 +28,7 @@ import { deleteAssignment, listAssignments, loadAssignment, saveAssignment, tran
|
|
|
28
28
|
import { listAgentRuns } from './agentruns.js';
|
|
29
29
|
import { reconcileAgentRun, reconcileDeadPidRunningAgentRunAtRead, reconcileStrandedFailureClaimAtRead, TERMINAL_STATUSES } from './agentrun-reconciler.js';
|
|
30
30
|
import { isObserverMode } from './observer-mode.js';
|
|
31
|
-
import { deleteRuntimeNote, listRuntimeNotes, saveRuntimeNote, } from './runtime.js';
|
|
31
|
+
import { deleteRuntimeNote, listRuntimeNotes, parkRuntimeNoteBackup, saveRuntimeNote, } from './runtime.js';
|
|
32
32
|
import { createSequence, deleteSequence, listSequences, updateSequence, } from './sequence.js';
|
|
33
33
|
import { createConstraint, createDecision, createTrap, } from './operations/memory-write.js';
|
|
34
34
|
import { deleteMemoryItem, findMemoryItemInChain, updateMemoryItem, } from './operations/memory-mutation.js';
|
|
@@ -853,10 +853,22 @@ export function removeEntity(name, id, cwd, purge = false) {
|
|
|
853
853
|
const note = notes.find((n) => n.id === id);
|
|
854
854
|
if (!note)
|
|
855
855
|
throw new EntityNotFoundError(name, id);
|
|
856
|
+
// trp_dc9ca61e — the tool contract says "archives by default", but this
|
|
857
|
+
// path hard-deleted regardless of `purge` (runtime_note has no lifecycle,
|
|
858
|
+
// so there was no soft state to land in). Default remove now parks the
|
|
859
|
+
// raw record under gc-backups — the same net the retention sweeps use —
|
|
860
|
+
// and fails CLOSED when the park is impossible: silently downgrading an
|
|
861
|
+
// archive into a hard-delete is the defect being fixed.
|
|
862
|
+
if (!purge) {
|
|
863
|
+
const backupPath = parkRuntimeNoteBackup(note, cwd);
|
|
864
|
+
if (!backupPath) {
|
|
865
|
+
throw new Error(`runtime_note '${id}' could not be archived to gc-backups; pass purge:true to hard-delete`);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
856
868
|
const ok = deleteRuntimeNote(note, cwd);
|
|
857
869
|
if (!ok)
|
|
858
870
|
throw new EntityNotFoundError(name, id);
|
|
859
|
-
return { entity: name, id, archived:
|
|
871
|
+
return { entity: name, id, archived: !purge, purged: purge };
|
|
860
872
|
}
|
|
861
873
|
case 'candidate': {
|
|
862
874
|
// Remove = archive to rejected. `purge` would delete the file; not exposed yet.
|
package/dist/core/hint-aging.js
CHANGED
|
@@ -123,7 +123,10 @@ export function ageStaleWarnings(warnings, cwd, options = {}) {
|
|
|
123
123
|
const overflow = ids.length - shown.length;
|
|
124
124
|
return `${ids.length} ${entity}${ids.length === 1 ? '' : 's'}: ${shown.join(', ')}${overflow > 0 ? ` +${overflow} more` : ''}`;
|
|
125
125
|
});
|
|
126
|
-
|
|
126
|
+
// trp_dc9ca61e — do not recommend bclaw_transition for runtime_notes: they
|
|
127
|
+
// have no lifecycle and the call errors. bclaw_remove (archive by default)
|
|
128
|
+
// is their retirement path.
|
|
129
|
+
aggregate = `${folded.length} stale item${folded.length === 1 ? '' : 's'} you've already been offered (${parts.join('; ')}) — bclaw_get each id to review; retire with bclaw_transition, or bclaw_remove for runtime_notes (no lifecycle).`;
|
|
127
130
|
}
|
|
128
131
|
return { warnings: detail, aggregate, served_ids, folded_ids };
|
|
129
132
|
}
|
package/dist/core/identity.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
247
|
+
const files = listCurrentSessionFiles(dir);
|
|
225
248
|
for (const file of files) {
|
|
249
|
+
const filepath = path.join(dir, file);
|
|
226
250
|
try {
|
|
227
|
-
|
|
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(
|
|
264
|
+
fs.unlinkSync(filepath);
|
|
234
265
|
removed++;
|
|
235
266
|
}
|
|
236
267
|
}
|
|
237
268
|
catch {
|
|
238
|
-
//
|
|
239
|
-
|
|
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
|
-
|
|
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') ||
|
package/dist/core/runtime.js
CHANGED
|
@@ -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);
|
package/dist/facts.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Generated by scripts/emit-site-facts.mjs at build time. Do not edit manually.
|
|
2
|
-
// Source: brainclaw v1.
|
|
2
|
+
// Source: brainclaw v1.26.0 on 2026-08-16T20:27:49.404Z
|
|
3
3
|
export const FACTS = {
|
|
4
|
-
"version": "1.
|
|
5
|
-
"generated_at": "2026-08-
|
|
4
|
+
"version": "1.26.0",
|
|
5
|
+
"generated_at": "2026-08-16T20:27:49.404Z",
|
|
6
6
|
"tools": {
|
|
7
7
|
"count": 70,
|
|
8
8
|
"published_count": 68,
|
|
@@ -477,8 +477,8 @@ export const FACTS = {
|
|
|
477
477
|
},
|
|
478
478
|
"bench": {
|
|
479
479
|
"schema": "brainclaw.bench.v1",
|
|
480
|
-
"generated_at": "2026-08-
|
|
481
|
-
"node_version": "v24.
|
|
480
|
+
"generated_at": "2026-08-16T20:27:47.271Z",
|
|
481
|
+
"node_version": "v24.19.0",
|
|
482
482
|
"platform": "linux-x64",
|
|
483
483
|
"repeats": 3,
|
|
484
484
|
"scenarios": [
|
|
@@ -486,7 +486,7 @@ export const FACTS = {
|
|
|
486
486
|
"name": "cold_onboard",
|
|
487
487
|
"volume": "empty",
|
|
488
488
|
"description": "fresh machine → init → first useful context. Baseline for time-to-first-value.",
|
|
489
|
-
"duration_ms_median":
|
|
489
|
+
"duration_ms_median": 75,
|
|
490
490
|
"payload_chars_median": 1640,
|
|
491
491
|
"payload_tokens_est_median": 410
|
|
492
492
|
},
|
|
@@ -494,17 +494,17 @@ export const FACTS = {
|
|
|
494
494
|
"name": "warm_work",
|
|
495
495
|
"volume": "medium",
|
|
496
496
|
"description": "bclaw_work consult over a real-shaped store (~200 plans / 500 handoffs / 450 claims).",
|
|
497
|
-
"duration_ms_median":
|
|
498
|
-
"payload_chars_median":
|
|
499
|
-
"payload_tokens_est_median":
|
|
497
|
+
"duration_ms_median": 132,
|
|
498
|
+
"payload_chars_median": 2626,
|
|
499
|
+
"payload_tokens_est_median": 657
|
|
500
500
|
},
|
|
501
501
|
{
|
|
502
502
|
"name": "first_edit",
|
|
503
503
|
"volume": "medium",
|
|
504
504
|
"description": "code_find + code_brief on the fresh-agent path (missing index, first touch).",
|
|
505
|
-
"duration_ms_median":
|
|
506
|
-
"payload_chars_median":
|
|
507
|
-
"payload_tokens_est_median":
|
|
505
|
+
"duration_ms_median": 11,
|
|
506
|
+
"payload_chars_median": 1305,
|
|
507
|
+
"payload_tokens_est_median": 326
|
|
508
508
|
}
|
|
509
509
|
]
|
|
510
510
|
}
|
package/dist/facts.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.
|
|
3
|
-
"generated_at": "2026-08-
|
|
2
|
+
"version": "1.26.0",
|
|
3
|
+
"generated_at": "2026-08-16T20:27:49.404Z",
|
|
4
4
|
"tools": {
|
|
5
5
|
"count": 70,
|
|
6
6
|
"published_count": 68,
|
|
@@ -475,8 +475,8 @@
|
|
|
475
475
|
},
|
|
476
476
|
"bench": {
|
|
477
477
|
"schema": "brainclaw.bench.v1",
|
|
478
|
-
"generated_at": "2026-08-
|
|
479
|
-
"node_version": "v24.
|
|
478
|
+
"generated_at": "2026-08-16T20:27:47.271Z",
|
|
479
|
+
"node_version": "v24.19.0",
|
|
480
480
|
"platform": "linux-x64",
|
|
481
481
|
"repeats": 3,
|
|
482
482
|
"scenarios": [
|
|
@@ -484,7 +484,7 @@
|
|
|
484
484
|
"name": "cold_onboard",
|
|
485
485
|
"volume": "empty",
|
|
486
486
|
"description": "fresh machine → init → first useful context. Baseline for time-to-first-value.",
|
|
487
|
-
"duration_ms_median":
|
|
487
|
+
"duration_ms_median": 75,
|
|
488
488
|
"payload_chars_median": 1640,
|
|
489
489
|
"payload_tokens_est_median": 410
|
|
490
490
|
},
|
|
@@ -492,17 +492,17 @@
|
|
|
492
492
|
"name": "warm_work",
|
|
493
493
|
"volume": "medium",
|
|
494
494
|
"description": "bclaw_work consult over a real-shaped store (~200 plans / 500 handoffs / 450 claims).",
|
|
495
|
-
"duration_ms_median":
|
|
496
|
-
"payload_chars_median":
|
|
497
|
-
"payload_tokens_est_median":
|
|
495
|
+
"duration_ms_median": 132,
|
|
496
|
+
"payload_chars_median": 2626,
|
|
497
|
+
"payload_tokens_est_median": 657
|
|
498
498
|
},
|
|
499
499
|
{
|
|
500
500
|
"name": "first_edit",
|
|
501
501
|
"volume": "medium",
|
|
502
502
|
"description": "code_find + code_brief on the fresh-agent path (missing index, first touch).",
|
|
503
|
-
"duration_ms_median":
|
|
504
|
-
"payload_chars_median":
|
|
505
|
-
"payload_tokens_est_median":
|
|
503
|
+
"duration_ms_median": 11,
|
|
504
|
+
"payload_chars_median": 1305,
|
|
505
|
+
"payload_tokens_est_median": 326
|
|
506
506
|
}
|
|
507
507
|
]
|
|
508
508
|
}
|
package/docs/code-map.md
CHANGED
|
@@ -136,35 +136,44 @@ call `bclaw_code_refresh` and retry.
|
|
|
136
136
|
|
|
137
137
|
## Freshness badge model
|
|
138
138
|
|
|
139
|
-
Every Code Map response
|
|
140
|
-
|
|
139
|
+
Every Code Map response has one top-level `freshness` field:
|
|
140
|
+
`fresh`, `stale`, `partial`, or `missing`. It is the synthetic index signal that
|
|
141
|
+
an agent uses to decide whether to refresh, and it has the same meaning on
|
|
142
|
+
`bclaw_work`, `bclaw_code_status`, `bclaw_code_find`, and `bclaw_code_brief`.
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
"freshness": "fresh",
|
|
147
|
+
"details": {
|
|
148
|
+
"index": {
|
|
149
|
+
"status": "fresh",
|
|
150
|
+
"stale_file_count": 0,
|
|
151
|
+
"partial_reason": null,
|
|
152
|
+
"git_head_changed": null
|
|
153
|
+
},
|
|
154
|
+
"spot_check": {
|
|
155
|
+
"status": "stale",
|
|
156
|
+
"checked_files": 1,
|
|
157
|
+
"stale_changed_files": ["src/example.ts"],
|
|
158
|
+
"deleted_files": [],
|
|
159
|
+
"unchecked_files": [],
|
|
160
|
+
"budget_exhausted": false,
|
|
161
|
+
"partial_reason": null
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
141
166
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
| `missing_index` | No index exists yet for this project. | `refresh --all` |
|
|
150
|
-
|
|
151
|
-
Staleness reasons are kept separate on purpose: a content change
|
|
152
|
-
(`stale_changed_files`) is independent from a config change (`stale_extractor`)
|
|
153
|
-
which is independent from a parser-binary change (`stale_grammar`). The badge
|
|
154
|
-
surfaces the dominant reason; `--json` output and the manifest carry the per-file
|
|
155
|
-
counts.
|
|
156
|
-
|
|
157
|
-
**Index freshness vs this call's spot-check.** `bclaw_code_status` reports the
|
|
158
|
-
*index* freshness (the manifest state). `bclaw_code_find` / `bclaw_code_brief`
|
|
159
|
-
additionally run a bounded, per-query *spot-check* of the files they actually
|
|
160
|
-
touch — so a single call can read `stale_changed_files` (a file it looked at
|
|
161
|
-
changed on disk) or `partial` (the spot-check hit its budget) even while the index
|
|
162
|
-
itself is `fresh`. When the call-level status diverges from the index, the badge
|
|
163
|
-
carries an `index_status` detail so the two are not confused, e.g.
|
|
164
|
-
`{ status: "partial", details: { index_status: "fresh", partial_reason:
|
|
165
|
-
"lazy_check_budget_exhausted" } }` reads as *"index fresh, this call's spot-check
|
|
166
|
-
incomplete (budget)"* — not a contradiction with a `fresh` `status()`.
|
|
167
|
+
`details.index` is the index diagnosis: its detailed `status` may be
|
|
168
|
+
`stale_changed_files`, `stale_extractor`, `stale_grammar`, or
|
|
169
|
+
`stale_git_head`. `details.spot_check` is a bounded, read-only observation of
|
|
170
|
+
the candidates touched by `find` or `brief`; it is `not_run` on `status` and on
|
|
171
|
+
a work section with no query. A stale or partial spot-check never silently
|
|
172
|
+
changes the shared top-level signal. It gives the agent precise evidence for an
|
|
173
|
+
explicit `bclaw_code_refresh(scope="changed")`, then a retry.
|
|
167
174
|
|
|
175
|
+
No read command parses files or refreshes the index. `bclaw_work` can suggest
|
|
176
|
+
that explicit refresh, but never performs it lazily.
|
|
168
177
|
## Lifecycle — pull-based, no daemon
|
|
169
178
|
|
|
170
179
|
Code Map never runs in the background and never auto-reindexes. The model is lazy
|