brainclaw 1.9.1 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +78 -25
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli.js +18 -1
  4. package/dist/commands/code-map.js +129 -0
  5. package/dist/commands/codev.js +7 -0
  6. package/dist/commands/dispatch-watch.js +1 -1
  7. package/dist/commands/doctor.js +3 -5
  8. package/dist/commands/loops-handlers.js +4 -1
  9. package/dist/commands/mcp-read-handlers.js +8 -0
  10. package/dist/commands/mcp.js +121 -1
  11. package/dist/commands/metrics.js +0 -1
  12. package/dist/commands/release-claims.js +1 -1
  13. package/dist/commands/run-profile.js +3 -2
  14. package/dist/commands/sequence.js +1 -1
  15. package/dist/commands/switch.js +100 -89
  16. package/dist/commands/sync.js +1 -1
  17. package/dist/commands/upgrade.js +0 -7
  18. package/dist/core/agent-context.js +1 -1
  19. package/dist/core/agent-files.js +13 -2
  20. package/dist/core/agent-integrations.js +3 -3
  21. package/dist/core/agent-registry.js +2 -2
  22. package/dist/core/assignments.js +12 -0
  23. package/dist/core/brainclaw-version.js +2 -2
  24. package/dist/core/code-map/backend.js +176 -0
  25. package/dist/core/code-map/core.js +81 -0
  26. package/dist/core/code-map/drafts.js +2 -0
  27. package/dist/core/code-map/extractor.js +29 -0
  28. package/dist/core/code-map/finalizer.js +191 -0
  29. package/dist/core/code-map/freshness.js +144 -0
  30. package/dist/core/code-map/ids.js +0 -0
  31. package/dist/core/code-map/importable.js +35 -0
  32. package/dist/core/code-map/indexes.js +197 -0
  33. package/dist/core/code-map/lang/java/imports.scm +17 -0
  34. package/dist/core/code-map/lang/java/index.js +254 -0
  35. package/dist/core/code-map/lang/java/tags.scm +48 -0
  36. package/dist/core/code-map/lang/php/imports.scm +21 -0
  37. package/dist/core/code-map/lang/php/index.js +251 -0
  38. package/dist/core/code-map/lang/php/tags.scm +44 -0
  39. package/dist/core/code-map/lang/provider.js +9 -0
  40. package/dist/core/code-map/lang/providers.js +24 -0
  41. package/dist/core/code-map/lang/python/imports.scm +90 -0
  42. package/dist/core/code-map/lang/python/index.js +364 -0
  43. package/dist/core/code-map/lang/python/tags.scm +81 -0
  44. package/dist/core/code-map/lang/query-runtime.js +374 -0
  45. package/dist/core/code-map/lang/registry.js +125 -0
  46. package/dist/core/code-map/lang/typescript/imports.scm +90 -0
  47. package/dist/core/code-map/lang/typescript/index.js +306 -0
  48. package/dist/core/code-map/lang/typescript/tags.js.scm +106 -0
  49. package/dist/core/code-map/lang/typescript/tags.scm +151 -0
  50. package/dist/core/code-map/lock.js +210 -0
  51. package/dist/core/code-map/materialized.js +51 -0
  52. package/dist/core/code-map/memory-reader.js +59 -0
  53. package/dist/core/code-map/paths.js +53 -0
  54. package/dist/core/code-map/query.js +599 -0
  55. package/dist/core/code-map/refresh.js +0 -0
  56. package/dist/core/code-map/resolve.js +177 -0
  57. package/dist/core/code-map/store.js +206 -0
  58. package/dist/core/code-map/types.js +293 -0
  59. package/dist/core/code-map/vocabulary.js +57 -0
  60. package/dist/core/code-map/wasm-loader.js +294 -0
  61. package/dist/core/code-map/work-section.js +206 -0
  62. package/dist/core/codev-rounds.js +4 -0
  63. package/dist/core/context.js +1 -1
  64. package/dist/core/cross-project.js +1 -1
  65. package/dist/core/dispatcher.js +0 -2
  66. package/dist/core/entity-operations.js +0 -3
  67. package/dist/core/execution-adapters.js +11 -10
  68. package/dist/core/execution-profile.js +58 -0
  69. package/dist/core/facade-schema.js +9 -0
  70. package/dist/core/ids.js +1 -1
  71. package/dist/core/instruction-templates.js +2 -0
  72. package/dist/core/instructions.js +0 -1
  73. package/dist/core/loops/lock.js +0 -3
  74. package/dist/core/mcp-command-resolution.js +3 -1
  75. package/dist/core/protocol-skills.js +5 -3
  76. package/dist/core/security-detectors.js +2 -2
  77. package/dist/core/security-extract.js +2 -2
  78. package/dist/core/store-resolution.js +41 -4
  79. package/dist/facts.js +9 -5
  80. package/dist/facts.json +8 -4
  81. package/dist/vendor/web-tree-sitter/tree-sitter.js +3980 -0
  82. package/dist/vendor/web-tree-sitter/tree-sitter.wasm +0 -0
  83. package/dist/wasm/tree-sitter-java.wasm +0 -0
  84. package/dist/wasm/tree-sitter-javascript.wasm +0 -0
  85. package/dist/wasm/tree-sitter-php.wasm +0 -0
  86. package/dist/wasm/tree-sitter-python.wasm +0 -0
  87. package/dist/wasm/tree-sitter-tsx.wasm +0 -0
  88. package/dist/wasm/tree-sitter-typescript.wasm +0 -0
  89. package/dist/wasm/tree-sitter.wasm +0 -0
  90. package/docs/cli.md +46 -8
  91. package/docs/code-map.md +209 -0
  92. package/docs/integrations/mcp.md +13 -6
  93. package/docs/mcp-schema-changelog.md +7 -3
  94. package/docs/quickstart.md +1 -1
  95. 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;
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}`);
@@ -1574,7 +1574,7 @@ function isExecutionSensitiveTarget(target) {
1574
1574
  function tokenise(input) {
1575
1575
  return input
1576
1576
  .toLowerCase()
1577
- .split(/[^a-z0-9_\/-]+/)
1577
+ .split(/[^a-z0-9_/-]+/)
1578
1578
  .map((x) => x.trim())
1579
1579
  .filter(Boolean);
1580
1580
  }
@@ -65,7 +65,7 @@ export function detectCrossProjectCycles(cwd) {
65
65
  const baseCwd = path.resolve(cwd ?? process.cwd());
66
66
  const cycles = [];
67
67
  function walk(currentCwd, visited) {
68
- let links = [];
68
+ let links;
69
69
  try {
70
70
  links = resolveCrossProjectLinks(currentCwd);
71
71
  }
@@ -53,7 +53,6 @@ import { sweepAssignments } from './assignment-sweeper.js';
53
53
  import { InboxMessageSchema } from './schema.js';
54
54
  import { generateId, nowISO } from './ids.js';
55
55
  import { applyHandoffUpdates } from '../commands/update-handoff.js';
56
- const MAX_INLINE_BRIEF_LENGTH = 4000;
57
56
  /**
58
57
  * Build a cross-platform env prefix for BRAINCLAW_CLAIM_ID. Delegates to
59
58
  * the centralised buildClaimEnvPrefix in src/core/execution-profile.ts
@@ -555,7 +554,6 @@ export function scoreAgents(agentPool, plan, activeClaims, cycleAssignments) {
555
554
  for (const claim of activeClaims) {
556
555
  claimCounts.set(claim.agent, (claimCounts.get(claim.agent) ?? 0) + 1);
557
556
  }
558
- const maxClaims = Math.max(1, ...claimCounts.values());
559
557
  return agentPool.map(agent => {
560
558
  // Factor 1: Preference — is this the plan's assignee?
561
559
  const preference = (plan.assignee === agent) ? 1.0 : 0.0;
@@ -212,9 +212,6 @@ function loadAll(name, cwd) {
212
212
  throw new EntityOperationUnsupportedError(name, 'find');
213
213
  }
214
214
  }
215
- function applyFilter(items, filter) {
216
- return applyFieldFilter(items, filter).filter((item) => passesProvenanceFilter(item, filter));
217
- }
218
215
  function applyFieldFilter(items, filter) {
219
216
  let result = items;
220
217
  if (filter.status) {
@@ -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
- const env = {
101
- ...process.env,
102
- // pln#562 step 5 truthful attribution: every commit a dispatched
103
- // worker makes is authored as the AGENT, not as the human whose
104
- // git config happens to be on the machine. invoke.env may override.
105
- ...buildGitAttributionEnv(options.agent),
106
- ...(invoke.env ?? {}),
107
- ...(options.claimId ? { BRAINCLAW_CLAIM_ID: options.claimId } : {}),
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))