brainclaw 1.9.1 → 1.10.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/README.md +47 -1
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +18 -1
- package/dist/commands/code-map.js +129 -0
- package/dist/commands/codev.js +7 -0
- package/dist/commands/mcp.js +121 -0
- package/dist/commands/run-profile.js +3 -2
- package/dist/commands/switch.js +100 -89
- package/dist/core/agent-files.js +12 -0
- package/dist/core/code-map/backend.js +123 -0
- package/dist/core/code-map/core.js +81 -0
- package/dist/core/code-map/drafts.js +2 -0
- package/dist/core/code-map/extractor.js +29 -0
- package/dist/core/code-map/finalizer.js +191 -0
- package/dist/core/code-map/freshness.js +108 -0
- package/dist/core/code-map/ids.js +0 -0
- package/dist/core/code-map/importable.js +35 -0
- package/dist/core/code-map/indexes.js +197 -0
- package/dist/core/code-map/lang/java/imports.scm +17 -0
- package/dist/core/code-map/lang/java/index.js +254 -0
- package/dist/core/code-map/lang/java/tags.scm +48 -0
- package/dist/core/code-map/lang/php/imports.scm +21 -0
- package/dist/core/code-map/lang/php/index.js +251 -0
- package/dist/core/code-map/lang/php/tags.scm +44 -0
- package/dist/core/code-map/lang/provider.js +9 -0
- package/dist/core/code-map/lang/providers.js +24 -0
- package/dist/core/code-map/lang/python/imports.scm +90 -0
- package/dist/core/code-map/lang/python/index.js +364 -0
- package/dist/core/code-map/lang/python/tags.scm +81 -0
- package/dist/core/code-map/lang/query-runtime.js +374 -0
- package/dist/core/code-map/lang/registry.js +125 -0
- package/dist/core/code-map/lang/typescript/imports.scm +90 -0
- package/dist/core/code-map/lang/typescript/index.js +306 -0
- package/dist/core/code-map/lang/typescript/tags.js.scm +106 -0
- package/dist/core/code-map/lang/typescript/tags.scm +151 -0
- package/dist/core/code-map/lock.js +210 -0
- package/dist/core/code-map/materialized.js +51 -0
- package/dist/core/code-map/memory-reader.js +59 -0
- package/dist/core/code-map/paths.js +53 -0
- package/dist/core/code-map/query.js +568 -0
- package/dist/core/code-map/refresh.js +0 -0
- package/dist/core/code-map/resolve.js +177 -0
- package/dist/core/code-map/store.js +206 -0
- package/dist/core/code-map/types.js +288 -0
- package/dist/core/code-map/vocabulary.js +57 -0
- package/dist/core/code-map/wasm-loader.js +294 -0
- package/dist/core/code-map/work-section.js +206 -0
- package/dist/core/codev-rounds.js +4 -0
- package/dist/core/execution-adapters.js +11 -10
- package/dist/core/execution-profile.js +58 -0
- package/dist/core/facade-schema.js +9 -0
- package/dist/core/instruction-templates.js +2 -0
- package/dist/core/mcp-command-resolution.js +3 -1
- package/dist/core/store-resolution.js +41 -4
- package/dist/facts.js +9 -5
- package/dist/facts.json +8 -4
- package/dist/vendor/web-tree-sitter/tree-sitter.js +3980 -0
- package/dist/vendor/web-tree-sitter/tree-sitter.wasm +0 -0
- package/dist/wasm/tree-sitter-java.wasm +0 -0
- package/dist/wasm/tree-sitter-javascript.wasm +0 -0
- package/dist/wasm/tree-sitter-php.wasm +0 -0
- package/dist/wasm/tree-sitter-python.wasm +0 -0
- package/dist/wasm/tree-sitter-tsx.wasm +0 -0
- package/dist/wasm/tree-sitter-typescript.wasm +0 -0
- package/dist/wasm/tree-sitter.wasm +0 -0
- package/docs/cli.md +46 -8
- package/docs/code-map.md +198 -0
- package/docs/integrations/mcp.md +13 -6
- package/docs/mcp-schema-changelog.md +7 -3
- package/docs/quickstart.md +1 -1
- package/package.json +11 -6
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brainclaw Code Map — universal vocabulary (spec §5, dec#108 #4).
|
|
3
|
+
*
|
|
4
|
+
* Defines the cross-language universal node-subtype and edge-kind sets, plus
|
|
5
|
+
* the namespacing rule for provider-specific extensions. A `NodeSubtype` /
|
|
6
|
+
* `EdgeKind` is EITHER one of the universal values OR a lowercase,
|
|
7
|
+
* provider-prefixed namespaced value of the form `provider.subtype`
|
|
8
|
+
* (e.g. `rust.trait`). The matching runtime validator lives in `types.ts`.
|
|
9
|
+
*
|
|
10
|
+
* P1a emits ONLY the existing universal values — providers MUST NOT introduce
|
|
11
|
+
* new universal subtypes/edge kinds here without a spec change.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Universal symbol subtypes shared across every language provider (spec §5).
|
|
15
|
+
* Ordering is documentary only; nothing depends on array order.
|
|
16
|
+
*/
|
|
17
|
+
export const UniversalNodeSubtypes = [
|
|
18
|
+
'function',
|
|
19
|
+
'method',
|
|
20
|
+
'constructor',
|
|
21
|
+
'class',
|
|
22
|
+
'type',
|
|
23
|
+
'interface',
|
|
24
|
+
'enum',
|
|
25
|
+
'variable',
|
|
26
|
+
'constant',
|
|
27
|
+
'field',
|
|
28
|
+
'property',
|
|
29
|
+
'namespace',
|
|
30
|
+
'package',
|
|
31
|
+
'component',
|
|
32
|
+
'hook',
|
|
33
|
+
'test',
|
|
34
|
+
'test_suite',
|
|
35
|
+
'macro',
|
|
36
|
+
'export',
|
|
37
|
+
];
|
|
38
|
+
/**
|
|
39
|
+
* Universal edge kinds shared across every language provider (spec §5).
|
|
40
|
+
* Ordering is documentary only; nothing depends on array order.
|
|
41
|
+
*/
|
|
42
|
+
export const UniversalEdgeKinds = [
|
|
43
|
+
'contains',
|
|
44
|
+
'defines',
|
|
45
|
+
'imports',
|
|
46
|
+
'exports',
|
|
47
|
+
'resolves_to',
|
|
48
|
+
'imports_symbol',
|
|
49
|
+
'tests_for',
|
|
50
|
+
'extends',
|
|
51
|
+
'implements',
|
|
52
|
+
'annotates',
|
|
53
|
+
'has_receiver',
|
|
54
|
+
];
|
|
55
|
+
/** Regex used by `types.ts` to validate provider-namespaced vocabulary values. */
|
|
56
|
+
export const NAMESPACED_VOCAB_RE = /^[a-z]+\.[a-z_]+$/;
|
|
57
|
+
//# sourceMappingURL=vocabulary.js.map
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree-sitter WASM engine + grammar loader (spec §6.2).
|
|
3
|
+
*
|
|
4
|
+
* Decided design points (spec §6.2):
|
|
5
|
+
* - web-tree-sitter is the engine glue (vendored into dist/vendor/; copied to
|
|
6
|
+
* dist/wasm/ alongside the grammar .wasm by scripts/copy-code-map-wasm.mjs).
|
|
7
|
+
* - Grammar .wasm (typescript / tsx / javascript) ship under dist/wasm/.
|
|
8
|
+
* - All assets are resolved by `new URL('./wasm/<file>.wasm', import.meta.url)`
|
|
9
|
+
* — NEVER cwd/__dirname — so the loader is worktree-safe under ESM.
|
|
10
|
+
* - Lazy-load on FIRST PARSE only. CRITICAL: the web-tree-sitter *JS glue* is a
|
|
11
|
+
* devDependency, so it is NEVER imported at module-load time — a static import
|
|
12
|
+
* would sit in the eager graph of cli.js / mcp.js and brick the ENTIRE CLI on
|
|
13
|
+
* a published package (devDeps dropped) even for `--version` / find / brief /
|
|
14
|
+
* status. Instead the glue is loaded via a DYNAMIC import inside `initEngine()`
|
|
15
|
+
* (the first-parse init path), preferring the VENDORED copy at
|
|
16
|
+
* `dist/vendor/web-tree-sitter/tree-sitter.js` (resolved via import.meta.url,
|
|
17
|
+
* bundled by scripts/copy-code-map-wasm.mjs) so the published package works at
|
|
18
|
+
* parse time too; it falls back to the bare 'web-tree-sitter' specifier when
|
|
19
|
+
* the vendored copy is absent (running from source).
|
|
20
|
+
* - `Parser.init()` runs once per process; each grammar `Language` is loaded
|
|
21
|
+
* only when a file of that language appears, then cached. Nothing here runs at
|
|
22
|
+
* import time or on the query path.
|
|
23
|
+
* - Hashes: `engineGlueHash()` = sha256 of the vendored engine .wasm;
|
|
24
|
+
* `grammarHash(lang)` = sha256 of the grammar .wasm. These feed manifest /
|
|
25
|
+
* shard freshness and are kept SEPARATE from extractor_config_hash.
|
|
26
|
+
*
|
|
27
|
+
* Packaging fallback: if the dist/wasm/ bundle is absent (e.g. running from
|
|
28
|
+
* source before the copy step has run, or the fallback packaging path), the
|
|
29
|
+
* loader resolves the .wasm assets from the installed node_modules
|
|
30
|
+
* devDependencies via `createRequire`. This keeps parse logic + tests working
|
|
31
|
+
* regardless of whether the dist bundling step has executed.
|
|
32
|
+
*/
|
|
33
|
+
import crypto from 'node:crypto';
|
|
34
|
+
import fs from 'node:fs';
|
|
35
|
+
import { fileURLToPath } from 'node:url';
|
|
36
|
+
import { createRequire } from 'node:module';
|
|
37
|
+
const require = createRequire(import.meta.url);
|
|
38
|
+
/** Grammar packages keyed by Code Map language tag. */
|
|
39
|
+
const GRAMMAR_WASM_BASENAME = {
|
|
40
|
+
javascript: 'tree-sitter-javascript.wasm',
|
|
41
|
+
typescript: 'tree-sitter-typescript.wasm',
|
|
42
|
+
tsx: 'tree-sitter-tsx.wasm',
|
|
43
|
+
};
|
|
44
|
+
/** node_modules fallback paths (resolved lazily; only used if dist bundle absent). */
|
|
45
|
+
const GRAMMAR_NODE_MODULES_SPEC = {
|
|
46
|
+
javascript: 'tree-sitter-wasms/out/tree-sitter-javascript.wasm',
|
|
47
|
+
typescript: 'tree-sitter-wasms/out/tree-sitter-typescript.wasm',
|
|
48
|
+
tsx: 'tree-sitter-wasms/out/tree-sitter-tsx.wasm',
|
|
49
|
+
};
|
|
50
|
+
const ENGINE_WASM_BASENAME = 'tree-sitter.wasm';
|
|
51
|
+
const ENGINE_NODE_MODULES_SPEC = 'web-tree-sitter/tree-sitter.wasm';
|
|
52
|
+
/**
|
|
53
|
+
* Narrow a runtime `CodeLang` onto the js-ts grammar key. `jsx` uses the tsx
|
|
54
|
+
* grammar (a strict superset of jsx + js). Any lang outside the js-ts set is not
|
|
55
|
+
* something THIS loader serves (it is some other provider's grammar) — default to
|
|
56
|
+
* `typescript` to stay total, matching the legacy fall-through behavior.
|
|
57
|
+
*/
|
|
58
|
+
function grammarLangFor(lang) {
|
|
59
|
+
switch (lang) {
|
|
60
|
+
case 'javascript':
|
|
61
|
+
case 'typescript':
|
|
62
|
+
case 'tsx':
|
|
63
|
+
return lang;
|
|
64
|
+
case 'jsx':
|
|
65
|
+
return 'tsx';
|
|
66
|
+
default:
|
|
67
|
+
return 'typescript';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Resolve a bundled wasm asset path. Prefers `dist/wasm/<basename>` resolved
|
|
72
|
+
* relative to THIS module via import.meta.url; falls back to the node_modules
|
|
73
|
+
* devDependency when the dist bundle is not present.
|
|
74
|
+
*/
|
|
75
|
+
function resolveWasmPath(distBasename, nodeModulesSpec) {
|
|
76
|
+
// dist-bundled location: <module dir>/wasm/<basename>. At runtime this module
|
|
77
|
+
// is dist/core/code-map/wasm-loader.js, so '../../wasm/<basename>' === dist/wasm/.
|
|
78
|
+
try {
|
|
79
|
+
const bundled = fileURLToPath(new URL(`../../wasm/${distBasename}`, import.meta.url));
|
|
80
|
+
if (fs.existsSync(bundled))
|
|
81
|
+
return bundled;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
/* fall through to node_modules */
|
|
85
|
+
}
|
|
86
|
+
// node_modules fallback (devDependency). Keeps parse logic + tests working
|
|
87
|
+
// even before the dist copy step has run.
|
|
88
|
+
return require.resolve(nodeModulesSpec);
|
|
89
|
+
}
|
|
90
|
+
function readWasm(path) {
|
|
91
|
+
return new Uint8Array(fs.readFileSync(path));
|
|
92
|
+
}
|
|
93
|
+
// --- generic grammar loader (shared engine seam for non-js-ts providers) ---
|
|
94
|
+
//
|
|
95
|
+
// P1b §3 (rule-of-two): a 2nd provider (Python) loads a grammar this js-ts loader
|
|
96
|
+
// does NOT own. Its grammar MUST come through the SAME initialized engine glue
|
|
97
|
+
// (getParser/getQueryClass/initEngine) — a fresh `web-tree-sitter` import is a
|
|
98
|
+
// distinct Emscripten instance and crashes on Query construction (trp_8df65ab7).
|
|
99
|
+
// These generic helpers expose exactly that: load/hash an arbitrary grammar .wasm
|
|
100
|
+
// (by dist basename + node_modules fallback spec) against the live engine. They are
|
|
101
|
+
// engine SEAMS, not orchestration — a new provider calls them from its own dir.
|
|
102
|
+
/** Cache for grammars loaded via the generic loader, keyed by dist basename. */
|
|
103
|
+
const genericGrammarCache = new Map();
|
|
104
|
+
const genericGrammarHashCache = new Map();
|
|
105
|
+
/**
|
|
106
|
+
* Load (and cache) an arbitrary grammar Language by its dist basename + a
|
|
107
|
+
* node_modules fallback spec, using the SAME engine glue + init as the js-ts
|
|
108
|
+
* grammars. Lazy: only called from a provider's parse path.
|
|
109
|
+
*/
|
|
110
|
+
export async function loadGrammarWasm(distBasename, nodeModulesSpec) {
|
|
111
|
+
const cached = genericGrammarCache.get(distBasename);
|
|
112
|
+
if (cached)
|
|
113
|
+
return cached;
|
|
114
|
+
await initEngine();
|
|
115
|
+
const glue = await loadEngineGlue();
|
|
116
|
+
const wasmPath = resolveWasmPath(distBasename, nodeModulesSpec);
|
|
117
|
+
const bytes = readWasm(wasmPath);
|
|
118
|
+
if (!genericGrammarHashCache.has(distBasename)) {
|
|
119
|
+
genericGrammarHashCache.set(distBasename, sha256(bytes));
|
|
120
|
+
}
|
|
121
|
+
const language = await glue.Language.load(bytes);
|
|
122
|
+
genericGrammarCache.set(distBasename, language);
|
|
123
|
+
return language;
|
|
124
|
+
}
|
|
125
|
+
/** sha256 of an arbitrary grammar .wasm (per-language tree_sitter_grammar_hash). */
|
|
126
|
+
export function grammarHashForWasm(distBasename, nodeModulesSpec) {
|
|
127
|
+
const cached = genericGrammarHashCache.get(distBasename);
|
|
128
|
+
if (cached)
|
|
129
|
+
return cached;
|
|
130
|
+
const wasmPath = resolveWasmPath(distBasename, nodeModulesSpec);
|
|
131
|
+
const h = sha256(readWasm(wasmPath));
|
|
132
|
+
genericGrammarHashCache.set(distBasename, h);
|
|
133
|
+
return h;
|
|
134
|
+
}
|
|
135
|
+
let glueModule = null;
|
|
136
|
+
let glueLoadPromise = null;
|
|
137
|
+
/**
|
|
138
|
+
* Dynamically import the web-tree-sitter JS glue on FIRST PARSE only. Prefers the
|
|
139
|
+
* vendored copy bundled into dist/vendor/web-tree-sitter/tree-sitter.js (resolved
|
|
140
|
+
* relative to this module via import.meta.url) so a published package — which has
|
|
141
|
+
* dropped the web-tree-sitter devDependency — still parses. Falls back to the
|
|
142
|
+
* bare 'web-tree-sitter' specifier when the vendored copy is absent (running from
|
|
143
|
+
* source / tests against node_modules).
|
|
144
|
+
*
|
|
145
|
+
* This function is the ONLY place that pulls web-tree-sitter into the runtime
|
|
146
|
+
* graph, and it runs strictly inside the parse path — so cli.js / mcp.js module
|
|
147
|
+
* load (and find / brief / status, which never parse) work with the engine glue
|
|
148
|
+
* entirely absent.
|
|
149
|
+
*/
|
|
150
|
+
async function loadEngineGlue() {
|
|
151
|
+
if (glueModule)
|
|
152
|
+
return glueModule;
|
|
153
|
+
if (glueLoadPromise)
|
|
154
|
+
return glueLoadPromise;
|
|
155
|
+
glueLoadPromise = (async () => {
|
|
156
|
+
// Vendored copy: <module dir>/../../vendor/web-tree-sitter/tree-sitter.js.
|
|
157
|
+
// At runtime this module is dist/core/code-map/wasm-loader.js, so
|
|
158
|
+
// '../../vendor/...' === dist/vendor/...
|
|
159
|
+
let mod;
|
|
160
|
+
const vendored = new URL('../../vendor/web-tree-sitter/tree-sitter.js', import.meta.url);
|
|
161
|
+
let vendoredExists = false;
|
|
162
|
+
try {
|
|
163
|
+
vendoredExists = fs.existsSync(fileURLToPath(vendored));
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
vendoredExists = false;
|
|
167
|
+
}
|
|
168
|
+
if (vendoredExists) {
|
|
169
|
+
mod = (await import(vendored.href));
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
// Fallback: bare specifier (node_modules devDependency). On a published
|
|
173
|
+
// package without the vendored copy this would throw — but the copy script
|
|
174
|
+
// always vendors the glue, so this branch is the from-source/tests path.
|
|
175
|
+
mod = (await import('web-tree-sitter'));
|
|
176
|
+
}
|
|
177
|
+
glueModule = mod;
|
|
178
|
+
return mod;
|
|
179
|
+
})();
|
|
180
|
+
return glueLoadPromise;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Parser constructor from the lazily-loaded engine glue (parse path only). Awaits
|
|
184
|
+
* {@link initEngine} first so any caller constructs a parser off an INITIALIZED
|
|
185
|
+
* engine — a parser built before `Parser.init()` would brick on first use.
|
|
186
|
+
*/
|
|
187
|
+
export async function getParser() {
|
|
188
|
+
await initEngine();
|
|
189
|
+
const glue = await loadEngineGlue();
|
|
190
|
+
return glue.Parser;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* `Query` constructor from the SAME lazily-loaded engine glue module that
|
|
194
|
+
* {@link initEngine}/{@link loadGrammar} run against, with the engine initialized
|
|
195
|
+
* first. CRITICAL: web-tree-sitter is shipped as a vendored copy under
|
|
196
|
+
* `dist/vendor/` AND exists as a node_modules devDependency; a fresh
|
|
197
|
+
* `import('web-tree-sitter')` resolves to a DISTINCT Emscripten module instance
|
|
198
|
+
* whose `Module` is never initialized by our `Parser.init({wasmBinary})` call.
|
|
199
|
+
* Constructing a `Query` against that un-inited instance throws
|
|
200
|
+
* `Cannot read properties of undefined (reading 'lengthBytesUTF8')`. Sourcing
|
|
201
|
+
* `Query` from `loadEngineGlue()` (same instance, after `initEngine()`) is the
|
|
202
|
+
* only correct path. (Regression fixed: P1a real-refresh WASM bug.)
|
|
203
|
+
*/
|
|
204
|
+
export async function getQueryClass() {
|
|
205
|
+
await initEngine();
|
|
206
|
+
const glue = await loadEngineGlue();
|
|
207
|
+
return glue.Query;
|
|
208
|
+
}
|
|
209
|
+
function sha256(bytes) {
|
|
210
|
+
return `sha256:${crypto.createHash('sha256').update(bytes).digest('hex')}`;
|
|
211
|
+
}
|
|
212
|
+
// --- Engine init (once per process) ---
|
|
213
|
+
let enginePath = null;
|
|
214
|
+
let engineInitPromise = null;
|
|
215
|
+
function engineWasmPath() {
|
|
216
|
+
if (enginePath === null) {
|
|
217
|
+
enginePath = resolveWasmPath(ENGINE_WASM_BASENAME, ENGINE_NODE_MODULES_SPEC);
|
|
218
|
+
}
|
|
219
|
+
return enginePath;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Initialize the Tree-sitter engine exactly once per process. The engine .wasm
|
|
223
|
+
* binary is read explicitly and passed via `wasmBinary` so we never depend on
|
|
224
|
+
* emscripten's cwd/script-dir resolution (worktree-safe).
|
|
225
|
+
*/
|
|
226
|
+
export function initEngine() {
|
|
227
|
+
if (!engineInitPromise) {
|
|
228
|
+
engineInitPromise = (async () => {
|
|
229
|
+
const glue = await loadEngineGlue();
|
|
230
|
+
const wasmBinary = readWasm(engineWasmPath());
|
|
231
|
+
await glue.Parser.init({ wasmBinary });
|
|
232
|
+
})();
|
|
233
|
+
}
|
|
234
|
+
return engineInitPromise;
|
|
235
|
+
}
|
|
236
|
+
// --- Grammar cache (lazy, per language) ---
|
|
237
|
+
const grammarCache = new Map();
|
|
238
|
+
const grammarHashCache = new Map();
|
|
239
|
+
/**
|
|
240
|
+
* Load (and cache) the grammar Language for a Code Map language. Initializes the
|
|
241
|
+
* engine first if needed. Lazy: only called from the parse loop.
|
|
242
|
+
*/
|
|
243
|
+
export async function loadGrammar(lang) {
|
|
244
|
+
const key = grammarLangFor(lang);
|
|
245
|
+
const cached = grammarCache.get(key);
|
|
246
|
+
if (cached)
|
|
247
|
+
return cached;
|
|
248
|
+
await initEngine();
|
|
249
|
+
const glue = await loadEngineGlue();
|
|
250
|
+
const path = resolveWasmPath(GRAMMAR_WASM_BASENAME[key], GRAMMAR_NODE_MODULES_SPEC[key]);
|
|
251
|
+
const bytes = readWasm(path);
|
|
252
|
+
if (!grammarHashCache.has(key))
|
|
253
|
+
grammarHashCache.set(key, sha256(bytes));
|
|
254
|
+
const language = await glue.Language.load(bytes);
|
|
255
|
+
grammarCache.set(key, language);
|
|
256
|
+
return language;
|
|
257
|
+
}
|
|
258
|
+
/** sha256 of the vendored engine glue .wasm (manifest.engine_glue_hash). */
|
|
259
|
+
export function engineGlueHash() {
|
|
260
|
+
return sha256(readWasm(engineWasmPath()));
|
|
261
|
+
}
|
|
262
|
+
/** sha256 of a grammar .wasm (per-language tree_sitter_grammar_hash). */
|
|
263
|
+
export function grammarHash(lang) {
|
|
264
|
+
const key = grammarLangFor(lang);
|
|
265
|
+
const cached = grammarHashCache.get(key);
|
|
266
|
+
if (cached)
|
|
267
|
+
return cached;
|
|
268
|
+
const path = resolveWasmPath(GRAMMAR_WASM_BASENAME[key], GRAMMAR_NODE_MODULES_SPEC[key]);
|
|
269
|
+
const h = sha256(readWasm(path));
|
|
270
|
+
grammarHashCache.set(key, h);
|
|
271
|
+
return h;
|
|
272
|
+
}
|
|
273
|
+
/** Map of canonical grammar names per language (manifest.languages.*). */
|
|
274
|
+
export const GRAMMAR_NAMES = {
|
|
275
|
+
javascript: 'tree-sitter-javascript',
|
|
276
|
+
typescript: 'tree-sitter-typescript',
|
|
277
|
+
tsx: 'tree-sitter-tsx',
|
|
278
|
+
};
|
|
279
|
+
export function grammarName(lang) {
|
|
280
|
+
return GRAMMAR_NAMES[grammarLangFor(lang)];
|
|
281
|
+
}
|
|
282
|
+
/** Test/maintenance seam: drop cached engine + grammars (not used in hot paths). */
|
|
283
|
+
export function __resetWasmCaches() {
|
|
284
|
+
grammarCache.clear();
|
|
285
|
+
grammarHashCache.clear();
|
|
286
|
+
genericGrammarCache.clear();
|
|
287
|
+
genericGrammarHashCache.clear();
|
|
288
|
+
engineInitPromise = null;
|
|
289
|
+
enginePath = null;
|
|
290
|
+
// NOTE: glueModule / glueLoadPromise are intentionally NOT reset — the dynamic
|
|
291
|
+
// import is process-global and re-importing is unnecessary; Parser.init is the
|
|
292
|
+
// re-runnable seam (gated by engineInitPromise above).
|
|
293
|
+
}
|
|
294
|
+
//# sourceMappingURL=wasm-loader.js.map
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Map ↔ bclaw_work integration seam (spec §10).
|
|
3
|
+
*
|
|
4
|
+
* This is the ONLY place bclaw_work touches Code Map. It is strictly opt-in:
|
|
5
|
+
* the section is produced only when the Code Map manifest carries
|
|
6
|
+
* `code_map_enabled: true`. When the flag is off (the P0 default, and the case
|
|
7
|
+
* for any project without a Code Map store), the helper returns `null` after a
|
|
8
|
+
* single cheap manifest stat — it never parses, never refreshes, never blocks,
|
|
9
|
+
* so bclaw_work latency is unchanged for the off path.
|
|
10
|
+
*
|
|
11
|
+
* Behaviour when enabled (spec §10 rules):
|
|
12
|
+
* - NEVER triggers a full (or any) refresh during bclaw_work.
|
|
13
|
+
* - Index missing -> a short `missing_index` hint (no results).
|
|
14
|
+
* - Index stale -> serve the find() results WITH the stale freshness badge.
|
|
15
|
+
* - Lock active -> wait at most `max_query_wait_ms` (default 2500ms) for the
|
|
16
|
+
* lock to clear; if it does not, serve last-known results if
|
|
17
|
+
* available, else skip with a `partial` badge. Never blocks
|
|
18
|
+
* bclaw_work beyond that bounded wait (rule §6 rule 8).
|
|
19
|
+
*/
|
|
20
|
+
import { readManifest } from './store.js';
|
|
21
|
+
import { readCodeLock, isLockAbandoned } from './lock.js';
|
|
22
|
+
import { codeMapDir, lockPath } from './paths.js';
|
|
23
|
+
import { JsonlBackend } from './backend.js';
|
|
24
|
+
/** Hard ceiling on how long the work-section may wait on an active lock (§6 rule 7, §10). */
|
|
25
|
+
export const WORK_SECTION_MAX_WAIT_MS = 2500;
|
|
26
|
+
/** Poll interval while waiting for a live lock to clear. */
|
|
27
|
+
const LOCK_POLL_MS = 100;
|
|
28
|
+
function defaultSleep(ms) {
|
|
29
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Is a competing Code Map lock currently *live* (not abandoned)? A live lock
|
|
33
|
+
* means a refresh is in flight; we must not read mid-write as fresh.
|
|
34
|
+
*/
|
|
35
|
+
function liveLockPresent(opts) {
|
|
36
|
+
const lock = readCodeLock(opts.cwd, opts.preferredDirName);
|
|
37
|
+
if (!lock)
|
|
38
|
+
return false;
|
|
39
|
+
const now = (opts.now ?? Date.now)();
|
|
40
|
+
// Reuse the §5.8 abandonment evaluation; an abandoned lock does not block.
|
|
41
|
+
const storeDir = codeMapDir(opts.cwd, opts.preferredDirName);
|
|
42
|
+
const lockFile = lockPath(opts.cwd, opts.preferredDirName);
|
|
43
|
+
try {
|
|
44
|
+
const abandoned = isLockAbandoned(lock, {
|
|
45
|
+
now,
|
|
46
|
+
isPidAlive: opts.isPidAlive ?? ((pid) => {
|
|
47
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
48
|
+
return false;
|
|
49
|
+
try {
|
|
50
|
+
process.kill(pid, 0);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}),
|
|
57
|
+
storeDir,
|
|
58
|
+
lockFile,
|
|
59
|
+
});
|
|
60
|
+
return !abandoned;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// If we cannot evaluate, treat as live (conservative): wait then degrade.
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Build the opt-in Code Map section for a bclaw_work response, or `null` when
|
|
69
|
+
* Code Map is not enabled for this project. See the module header for the full
|
|
70
|
+
* behaviour contract (spec §10).
|
|
71
|
+
*
|
|
72
|
+
* The single live call-site is `bclaw_work` in src/commands/mcp.ts — search for
|
|
73
|
+
* `codeMapWorkSection(`.
|
|
74
|
+
*/
|
|
75
|
+
export async function codeMapWorkSection(cwd, opts = {}) {
|
|
76
|
+
const ctx = { ...opts, cwd: opts.cwd ?? cwd };
|
|
77
|
+
// OFF PATH: one cheap manifest stat. No store / flag off -> nothing at all.
|
|
78
|
+
const manifest = readManifest(ctx.cwd, ctx.preferredDirName);
|
|
79
|
+
if (!manifest || manifest.code_map_enabled !== true) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const backend = ctx.backend ?? new JsonlBackend();
|
|
83
|
+
const query = (ctx.query ?? '').trim();
|
|
84
|
+
const limit = ctx.limit ?? 8;
|
|
85
|
+
// LOCK PATH: bounded wait for an active (live) lock to clear (§10).
|
|
86
|
+
let lockWaitMs;
|
|
87
|
+
if (liveLockPresent(ctx)) {
|
|
88
|
+
const sleep = ctx.sleep ?? defaultSleep;
|
|
89
|
+
const now = ctx.now ?? Date.now;
|
|
90
|
+
const deadline = now() + WORK_SECTION_MAX_WAIT_MS;
|
|
91
|
+
while (now() < deadline && liveLockPresent(ctx)) {
|
|
92
|
+
await sleep(LOCK_POLL_MS);
|
|
93
|
+
}
|
|
94
|
+
lockWaitMs = WORK_SECTION_MAX_WAIT_MS - Math.max(0, deadline - now());
|
|
95
|
+
if (liveLockPresent(ctx)) {
|
|
96
|
+
// Still locked after the bounded wait — degrade to partial. Serve
|
|
97
|
+
// last-known matches if a query is present and the index is readable,
|
|
98
|
+
// else skip the section content with a partial badge. Never block.
|
|
99
|
+
if (query) {
|
|
100
|
+
try {
|
|
101
|
+
const out = await backend.find({
|
|
102
|
+
query,
|
|
103
|
+
limit,
|
|
104
|
+
cwd: ctx.cwd,
|
|
105
|
+
preferredDirName: ctx.preferredDirName,
|
|
106
|
+
});
|
|
107
|
+
return {
|
|
108
|
+
enabled: true,
|
|
109
|
+
matches: out.matches,
|
|
110
|
+
freshness_badge: {
|
|
111
|
+
status: 'partial',
|
|
112
|
+
details: { partial_reason: 'code_map_lock_active', lock_wait_ms: lockWaitMs },
|
|
113
|
+
},
|
|
114
|
+
lock_wait_ms: lockWaitMs,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
/* fall through to skip */
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
enabled: true,
|
|
123
|
+
matches: [],
|
|
124
|
+
freshness_badge: {
|
|
125
|
+
status: 'partial',
|
|
126
|
+
details: { partial_reason: 'code_map_lock_active', lock_wait_ms: lockWaitMs },
|
|
127
|
+
},
|
|
128
|
+
lock_wait_ms: lockWaitMs,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// MISSING INDEX: nothing parsed yet (spec §10). Short hint, no results.
|
|
133
|
+
if (manifest.freshness.status === 'missing_index') {
|
|
134
|
+
return {
|
|
135
|
+
enabled: true,
|
|
136
|
+
missing_index: 'Code Map index is empty for this project. Run `brainclaw code-map refresh --all` (or bclaw_code_refresh) before relying on find/brief.',
|
|
137
|
+
matches: [],
|
|
138
|
+
freshness_badge: { status: 'missing_index', details: {} },
|
|
139
|
+
...(lockWaitMs !== undefined ? { lock_wait_ms: lockWaitMs } : {}),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// FRESH or STALE: run find() (read-only, never refreshes). The backend's
|
|
143
|
+
// lazy read-path check (§6.1) returns the true freshness badge, so stale
|
|
144
|
+
// results are surfaced WITH the stale badge rather than hidden.
|
|
145
|
+
if (!query) {
|
|
146
|
+
return {
|
|
147
|
+
enabled: true,
|
|
148
|
+
matches: [],
|
|
149
|
+
freshness_badge: {
|
|
150
|
+
status: manifest.freshness.status,
|
|
151
|
+
details: {
|
|
152
|
+
stale_file_count: manifest.freshness.stale_file_count,
|
|
153
|
+
partial_reason: manifest.freshness.partial_reason,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
...(lockWaitMs !== undefined ? { lock_wait_ms: lockWaitMs } : {}),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const out = await backend.find({
|
|
160
|
+
query,
|
|
161
|
+
limit,
|
|
162
|
+
cwd: ctx.cwd,
|
|
163
|
+
preferredDirName: ctx.preferredDirName,
|
|
164
|
+
});
|
|
165
|
+
return {
|
|
166
|
+
enabled: true,
|
|
167
|
+
matches: out.matches,
|
|
168
|
+
freshness_badge: out.freshness_badge,
|
|
169
|
+
...(lockWaitMs !== undefined ? { lock_wait_ms: lockWaitMs } : {}),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Derive the onboarding/freshness next_action(s) from a Code Map work section
|
|
174
|
+
* (pln#588 adoption). The passive `missing_index` hint string was easy for agents
|
|
175
|
+
* to skip; promoting the refresh to a first-class `next_action` makes a fresh or
|
|
176
|
+
* stale project's first `bclaw_work` actively nudge the agent to build/update the
|
|
177
|
+
* index — without ever refreshing here. Pure + side-effect-free so it can be
|
|
178
|
+
* unit-locked; the bclaw_work handler spreads the result into its next_actions.
|
|
179
|
+
* - missing_index -> build the whole index (`scope: all`)
|
|
180
|
+
* - stale_* -> refresh changed files (`scope: changed`)
|
|
181
|
+
* - fresh/partial/null -> nothing (don't nag on a usable index or a transient lock)
|
|
182
|
+
*/
|
|
183
|
+
export function codeMapRefreshNextActions(section) {
|
|
184
|
+
if (!section)
|
|
185
|
+
return [];
|
|
186
|
+
if (section.missing_index) {
|
|
187
|
+
return [
|
|
188
|
+
{
|
|
189
|
+
tool: 'bclaw_code_refresh',
|
|
190
|
+
args: { scope: 'all' },
|
|
191
|
+
when: 'Code Map index is empty — build it so bclaw_code_find/brief can orient you before editing',
|
|
192
|
+
},
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
if (section.freshness_badge?.status?.startsWith('stale_')) {
|
|
196
|
+
return [
|
|
197
|
+
{
|
|
198
|
+
tool: 'bclaw_code_refresh',
|
|
199
|
+
args: { scope: 'changed' },
|
|
200
|
+
when: 'Code Map is stale — refresh changed files before relying on bclaw_code_find/brief',
|
|
201
|
+
},
|
|
202
|
+
];
|
|
203
|
+
}
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=work-section.js.map
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { saveIdeationRound, loadIdeationRound, listIdeationRounds } from './ideation.js';
|
|
8
8
|
import { recordResponse } from './codev-metrics.js';
|
|
9
9
|
import { buildPositionPrompt, buildReactionPrompt, buildConvergencePrompt } from './codev-prompts.js';
|
|
10
|
+
import { buildWorkerIdentityEnv } from './execution-profile.js';
|
|
10
11
|
import { spawn, spawnSync } from 'node:child_process';
|
|
11
12
|
import fs from 'node:fs';
|
|
12
13
|
import path from 'node:path';
|
|
@@ -55,6 +56,9 @@ function spawnAgent(binaryPath, agentName, prompt, personaName, cwd, outputFile,
|
|
|
55
56
|
detached: true,
|
|
56
57
|
stdio: ['ignore', outFd, 'ignore'],
|
|
57
58
|
cwd,
|
|
59
|
+
// F7 (trp_0e5150d3): scrub coordinator identity — this spawn previously
|
|
60
|
+
// inherited the full parent env.
|
|
61
|
+
env: buildWorkerIdentityEnv(process.env, { agent: agentName }),
|
|
58
62
|
});
|
|
59
63
|
child.on('error', (err) => {
|
|
60
64
|
console.warn(` ⚠ Spawn error for ${agentName}/${personaName}: ${err.message}`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn, execFileSync } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { buildClaimEnvPrefix } from './execution-profile.js';
|
|
4
|
+
import { buildClaimEnvPrefix, buildWorkerIdentityEnv } from './execution-profile.js';
|
|
5
5
|
import { getCapabilityProfile } from './agent-capability.js';
|
|
6
6
|
import { nowISO } from './ids.js';
|
|
7
7
|
import { ensureRuntimeDirs, getRuntimeLogPath, getRuntimeSignalPath, } from './runtime-signals.js';
|
|
@@ -97,15 +97,16 @@ export class CliExecutionAdapter {
|
|
|
97
97
|
}
|
|
98
98
|
start(invoke, options) {
|
|
99
99
|
const isWin32 = process.platform === 'win32';
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
100
|
+
// F7 (trp_0e5150d3): route worker env through buildWorkerIdentityEnv so the
|
|
101
|
+
// worker is an independent agent — coordinator identity (BRAINCLAW_AGENT*,
|
|
102
|
+
// SESSION_ID, PROJECT) is scrubbed LAST and cannot be reintroduced by
|
|
103
|
+
// invoke.env. pln#562 step 5 — truthful git attribution (worker authors its
|
|
104
|
+
// own commits) is merged before the scrub. BRAINCLAW_CWD is preserved (D1a).
|
|
105
|
+
const env = buildWorkerIdentityEnv(process.env, {
|
|
106
|
+
agent: options.agent,
|
|
107
|
+
claimId: options.claimId,
|
|
108
|
+
extraEnv: { ...buildGitAttributionEnv(options.agent), ...(invoke.env ?? {}) },
|
|
109
|
+
});
|
|
109
110
|
if (invoke.promptDelivery === 'temp_file' && invoke.tempFilePath && invoke.promptText) {
|
|
110
111
|
const dir = path.dirname(invoke.tempFilePath);
|
|
111
112
|
if (!fs.existsSync(dir))
|
|
@@ -247,6 +247,64 @@ export function buildClaimEnvPrefix(claimId, options) {
|
|
|
247
247
|
default: return `BRAINCLAW_CLAIM_ID=${claimId} `;
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* F7 (trp_0e5150d3): build the env for a SPAWNED worker so it is an INDEPENDENT
|
|
252
|
+
* agent, not a clone of the coordinator. The coordinator's process.env carries
|
|
253
|
+
* identity-bearing state (BRAINCLAW_AGENT*, BRAINCLAW_SESSION_ID, BRAINCLAW_PROJECT);
|
|
254
|
+
* inherited by an MCP-capable worker it would make the worker read/mutate the
|
|
255
|
+
* coordinator's session-scoped active project and identify AS the coordinator.
|
|
256
|
+
*
|
|
257
|
+
* Merge order (Codex cadrage review — batch 2): baseEnv → extraEnv (git
|
|
258
|
+
* attribution + invoke.env) → worker identity (agent name, claim) → SCRUB the
|
|
259
|
+
* forbidden coordinator keys LAST, so a deliberate or accidental extraEnv cannot
|
|
260
|
+
* reintroduce them. This helper is the SINGLE place worker env is built; every
|
|
261
|
+
* agent-spawn site must route through it.
|
|
262
|
+
*
|
|
263
|
+
* BRAINCLAW_CWD is intentionally PRESERVED (D1 = (a)): a worker's worktree lives
|
|
264
|
+
* outside the monorepo and has no .brainclaw, so it must anchor to the shared
|
|
265
|
+
* monorepo-root store the coordinator pointed at; the claim scopes the work. A
|
|
266
|
+
* caller override via extraEnv.BRAINCLAW_CWD is honoured (deliberate, not leaked).
|
|
267
|
+
*/
|
|
268
|
+
export function buildWorkerIdentityEnv(baseEnv, options = {}) {
|
|
269
|
+
const env = {};
|
|
270
|
+
for (const [key, value] of Object.entries(baseEnv)) {
|
|
271
|
+
if (typeof value === 'string')
|
|
272
|
+
env[key] = value;
|
|
273
|
+
}
|
|
274
|
+
if (options.extraEnv) {
|
|
275
|
+
for (const [key, value] of Object.entries(options.extraEnv)) {
|
|
276
|
+
if (typeof value === 'string')
|
|
277
|
+
env[key] = value;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// Worker identity — truthful attribution, mirrors buildGitAttributionEnv.
|
|
281
|
+
const agent = options.agent?.trim();
|
|
282
|
+
if (agent) {
|
|
283
|
+
env.BRAINCLAW_AGENT = agent;
|
|
284
|
+
env.BRAINCLAW_AGENT_NAME = agent;
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
// No target agent → don't leak the coordinator's identity either.
|
|
288
|
+
delete env.BRAINCLAW_AGENT;
|
|
289
|
+
delete env.BRAINCLAW_AGENT_NAME;
|
|
290
|
+
}
|
|
291
|
+
// Claim ownership is explicit: adopt the supplied claim, otherwise DELETE any
|
|
292
|
+
// inherited parent BRAINCLAW_CLAIM_ID so an unclaimed/dry-run worker (e.g. the
|
|
293
|
+
// codev/codev-rounds call sites that pass only { agent }) never adopts the
|
|
294
|
+
// coordinator's claim (Codex final review F7 finding).
|
|
295
|
+
if (options.claimId && options.claimId !== '(dry-run)') {
|
|
296
|
+
env.BRAINCLAW_CLAIM_ID = options.claimId;
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
delete env.BRAINCLAW_CLAIM_ID;
|
|
300
|
+
}
|
|
301
|
+
// SCRUB LAST — coordinator identity must never survive into the worker, even if
|
|
302
|
+
// baseEnv or extraEnv set it. (BRAINCLAW_CWD is deliberately NOT scrubbed.)
|
|
303
|
+
delete env.BRAINCLAW_AGENT_ID;
|
|
304
|
+
delete env.BRAINCLAW_SESSION_ID;
|
|
305
|
+
delete env.BRAINCLAW_PROJECT;
|
|
306
|
+
return env;
|
|
307
|
+
}
|
|
250
308
|
// ── Verification helper (used by setup / doctor) ───────────────────────────
|
|
251
309
|
/**
|
|
252
310
|
* Try `node --version` from the resolved profile's `node_path` to confirm
|