byterover-cli 3.1.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +17 -0
  2. package/dist/agent/infra/agent/agent-schemas.d.ts +8 -0
  3. package/dist/agent/infra/agent/agent-schemas.js +1 -0
  4. package/dist/agent/infra/sandbox/curate-service.js +14 -0
  5. package/dist/agent/infra/sandbox/sandbox-service.js +1 -0
  6. package/dist/agent/infra/sandbox/tools-sdk.d.ts +10 -0
  7. package/dist/agent/infra/sandbox/tools-sdk.js +9 -1
  8. package/dist/agent/infra/tools/implementations/search-knowledge-service.js +226 -103
  9. package/dist/agent/infra/tools/implementations/write-file-tool.d.ts +2 -1
  10. package/dist/agent/infra/tools/implementations/write-file-tool.js +16 -2
  11. package/dist/agent/infra/tools/tool-registry.js +1 -1
  12. package/dist/agent/infra/tools/write-guard.d.ts +11 -0
  13. package/dist/agent/infra/tools/write-guard.js +48 -0
  14. package/dist/agent/resources/prompts/system-prompt.yml +9 -0
  15. package/dist/agent/resources/tools/expand_knowledge.txt +4 -0
  16. package/dist/agent/resources/tools/search_knowledge.txt +11 -1
  17. package/dist/oclif/commands/curate/index.d.ts +1 -0
  18. package/dist/oclif/commands/curate/index.js +19 -4
  19. package/dist/oclif/commands/curate/view.js +2 -2
  20. package/dist/oclif/commands/main.js +13 -0
  21. package/dist/oclif/commands/query.d.ts +1 -0
  22. package/dist/oclif/commands/query.js +19 -4
  23. package/dist/oclif/commands/search.d.ts +20 -0
  24. package/dist/oclif/commands/search.js +186 -0
  25. package/dist/oclif/commands/source/add.d.ts +12 -0
  26. package/dist/oclif/commands/source/add.js +42 -0
  27. package/dist/oclif/commands/source/index.d.ts +6 -0
  28. package/dist/oclif/commands/source/index.js +8 -0
  29. package/dist/oclif/commands/source/list.d.ts +6 -0
  30. package/dist/oclif/commands/source/list.js +32 -0
  31. package/dist/oclif/commands/source/remove.d.ts +9 -0
  32. package/dist/oclif/commands/source/remove.js +33 -0
  33. package/dist/oclif/commands/status.d.ts +5 -1
  34. package/dist/oclif/commands/status.js +45 -6
  35. package/dist/oclif/commands/worktree/add.d.ts +12 -0
  36. package/dist/oclif/commands/worktree/add.js +44 -0
  37. package/dist/oclif/commands/worktree/index.d.ts +6 -0
  38. package/dist/oclif/commands/worktree/index.js +8 -0
  39. package/dist/oclif/commands/worktree/list.d.ts +6 -0
  40. package/dist/oclif/commands/worktree/list.js +28 -0
  41. package/dist/oclif/commands/worktree/remove.d.ts +9 -0
  42. package/dist/oclif/commands/worktree/remove.js +35 -0
  43. package/dist/oclif/hooks/init/validate-brv-config.js +4 -0
  44. package/dist/oclif/lib/daemon-client.d.ts +4 -2
  45. package/dist/oclif/lib/daemon-client.js +19 -4
  46. package/dist/oclif/lib/search-format.d.ts +10 -0
  47. package/dist/oclif/lib/search-format.js +25 -0
  48. package/dist/oclif/lib/task-client.d.ts +6 -0
  49. package/dist/oclif/lib/task-client.js +10 -3
  50. package/dist/server/constants.d.ts +7 -1
  51. package/dist/server/constants.js +10 -0
  52. package/dist/server/core/domain/client/client-info.d.ts +7 -0
  53. package/dist/server/core/domain/client/client-info.js +11 -0
  54. package/dist/server/core/domain/errors/task-error.d.ts +2 -2
  55. package/dist/server/core/domain/errors/task-error.js +5 -4
  56. package/dist/server/core/domain/project/worktrees-schema.d.ts +29 -0
  57. package/dist/server/core/domain/project/worktrees-schema.js +17 -0
  58. package/dist/server/core/domain/source/source-operations.d.ts +31 -0
  59. package/dist/server/core/domain/source/source-operations.js +201 -0
  60. package/dist/server/core/domain/source/source-schema.d.ts +94 -0
  61. package/dist/server/core/domain/source/source-schema.js +121 -0
  62. package/dist/server/core/domain/transport/schemas.d.ts +18 -10
  63. package/dist/server/core/domain/transport/schemas.js +7 -3
  64. package/dist/server/core/domain/transport/task-info.d.ts +2 -0
  65. package/dist/server/core/interfaces/client/i-client-manager.d.ts +13 -0
  66. package/dist/server/core/interfaces/executor/i-curate-executor.d.ts +4 -0
  67. package/dist/server/core/interfaces/executor/i-folder-pack-executor.d.ts +7 -3
  68. package/dist/server/core/interfaces/executor/i-query-executor.d.ts +2 -0
  69. package/dist/server/core/interfaces/executor/i-search-executor.d.ts +34 -0
  70. package/dist/server/core/interfaces/executor/i-search-executor.js +1 -0
  71. package/dist/server/core/interfaces/executor/index.d.ts +1 -0
  72. package/dist/server/core/interfaces/executor/index.js +1 -0
  73. package/dist/server/infra/client/client-manager.d.ts +1 -0
  74. package/dist/server/infra/client/client-manager.js +16 -0
  75. package/dist/server/infra/daemon/agent-process.js +35 -12
  76. package/dist/server/infra/executor/curate-executor.js +4 -2
  77. package/dist/server/infra/executor/direct-search-responder.js +5 -1
  78. package/dist/server/infra/executor/folder-pack-executor.js +23 -12
  79. package/dist/server/infra/executor/query-executor.d.ts +23 -0
  80. package/dist/server/infra/executor/query-executor.js +115 -21
  81. package/dist/server/infra/executor/search-executor.d.ts +17 -0
  82. package/dist/server/infra/executor/search-executor.js +30 -0
  83. package/dist/server/infra/mcp/mcp-mode-detector.d.ts +7 -5
  84. package/dist/server/infra/mcp/mcp-mode-detector.js +11 -18
  85. package/dist/server/infra/mcp/mcp-server.d.ts +1 -0
  86. package/dist/server/infra/mcp/mcp-server.js +11 -6
  87. package/dist/server/infra/mcp/tools/brv-curate-tool.d.ts +2 -1
  88. package/dist/server/infra/mcp/tools/brv-curate-tool.js +9 -16
  89. package/dist/server/infra/mcp/tools/brv-query-tool.d.ts +2 -1
  90. package/dist/server/infra/mcp/tools/brv-query-tool.js +9 -16
  91. package/dist/server/infra/mcp/tools/mcp-project-context.d.ts +11 -0
  92. package/dist/server/infra/mcp/tools/mcp-project-context.js +54 -0
  93. package/dist/server/infra/process/connection-coordinator.js +11 -0
  94. package/dist/server/infra/process/feature-handlers.js +4 -1
  95. package/dist/server/infra/process/task-router.d.ts +1 -0
  96. package/dist/server/infra/process/task-router.js +60 -5
  97. package/dist/server/infra/project/resolve-project.d.ts +106 -0
  98. package/dist/server/infra/project/resolve-project.js +473 -0
  99. package/dist/server/infra/transport/handlers/index.d.ts +4 -0
  100. package/dist/server/infra/transport/handlers/index.js +2 -0
  101. package/dist/server/infra/transport/handlers/pull-handler.js +3 -3
  102. package/dist/server/infra/transport/handlers/push-handler.js +3 -3
  103. package/dist/server/infra/transport/handlers/source-handler.d.ts +12 -0
  104. package/dist/server/infra/transport/handlers/source-handler.js +37 -0
  105. package/dist/server/infra/transport/handlers/status-handler.js +76 -27
  106. package/dist/server/infra/transport/handlers/worktree-handler.d.ts +12 -0
  107. package/dist/server/infra/transport/handlers/worktree-handler.js +67 -0
  108. package/dist/server/infra/transport/transport-connector.d.ts +10 -4
  109. package/dist/server/infra/transport/transport-connector.js +2 -2
  110. package/dist/server/templates/skill/SKILL.md +25 -5
  111. package/dist/server/utils/path-utils.d.ts +5 -0
  112. package/dist/server/utils/path-utils.js +11 -1
  113. package/dist/shared/transport/events/client-events.d.ts +3 -0
  114. package/dist/shared/transport/events/client-events.js +3 -0
  115. package/dist/shared/transport/events/index.d.ts +13 -0
  116. package/dist/shared/transport/events/index.js +9 -0
  117. package/dist/shared/transport/events/source-events.d.ts +30 -0
  118. package/dist/shared/transport/events/source-events.js +5 -0
  119. package/dist/shared/transport/events/status-events.d.ts +5 -0
  120. package/dist/shared/transport/events/task-events.d.ts +4 -1
  121. package/dist/shared/transport/events/worktree-events.d.ts +31 -0
  122. package/dist/shared/transport/events/worktree-events.js +5 -0
  123. package/dist/shared/transport/search-content.d.ts +28 -0
  124. package/dist/shared/transport/search-content.js +38 -0
  125. package/dist/shared/transport/types/dto.d.ts +20 -1
  126. package/dist/tui/features/commands/definitions/index.js +6 -0
  127. package/dist/tui/features/commands/definitions/source-add.d.ts +2 -0
  128. package/dist/tui/features/commands/definitions/source-add.js +48 -0
  129. package/dist/tui/features/commands/definitions/source-list.d.ts +2 -0
  130. package/dist/tui/features/commands/definitions/source-list.js +47 -0
  131. package/dist/tui/features/commands/definitions/source-remove.d.ts +2 -0
  132. package/dist/tui/features/commands/definitions/source-remove.js +38 -0
  133. package/dist/tui/features/commands/definitions/source.d.ts +2 -0
  134. package/dist/tui/features/commands/definitions/source.js +8 -0
  135. package/dist/tui/features/commands/definitions/worktree-add.d.ts +2 -0
  136. package/dist/tui/features/commands/definitions/worktree-add.js +35 -0
  137. package/dist/tui/features/commands/definitions/worktree-list.d.ts +2 -0
  138. package/dist/tui/features/commands/definitions/worktree-list.js +36 -0
  139. package/dist/tui/features/commands/definitions/worktree-remove.d.ts +2 -0
  140. package/dist/tui/features/commands/definitions/worktree-remove.js +33 -0
  141. package/dist/tui/features/commands/definitions/worktree.d.ts +2 -0
  142. package/dist/tui/features/commands/definitions/worktree.js +8 -0
  143. package/dist/tui/features/curate/api/create-curate-task.js +3 -1
  144. package/dist/tui/features/query/api/create-query-task.js +3 -1
  145. package/dist/tui/features/source/api/source-api.d.ts +4 -0
  146. package/dist/tui/features/source/api/source-api.js +22 -0
  147. package/dist/tui/features/status/api/get-status.js +2 -1
  148. package/dist/tui/features/status/utils/format-status.js +28 -1
  149. package/dist/tui/features/transport/components/transport-initializer.js +36 -1
  150. package/dist/tui/features/worktree/api/worktree-api.d.ts +4 -0
  151. package/dist/tui/features/worktree/api/worktree-api.js +22 -0
  152. package/dist/tui/repl-startup.d.ts +2 -0
  153. package/dist/tui/repl-startup.js +5 -3
  154. package/dist/tui/stores/transport-store.d.ts +6 -0
  155. package/dist/tui/stores/transport-store.js +6 -0
  156. package/dist/tui/utils/error-messages.js +2 -2
  157. package/oclif.manifest.json +380 -36
  158. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
- import { join } from 'node:path';
1
+ import { join, relative } from 'node:path';
2
2
  import { ABSTRACT_EXTENSION, BRV_DIR, CONTEXT_FILE_EXTENSION, CONTEXT_TREE_DIR } from '../../constants.js';
3
+ import { loadSources } from '../../core/domain/source/source-schema.js';
3
4
  import { isDerivedArtifact } from '../context-tree/derived-artifact.js';
4
5
  import { FileContextTreeManifestService } from '../context-tree/file-context-tree-manifest-service.js';
5
6
  import { canRespondDirectly, formatDirectResponse, formatNotFoundResponse, } from './direct-search-responder.js';
@@ -45,15 +46,16 @@ export class QueryExecutor {
45
46
  }
46
47
  }
47
48
  async executeWithAgent(agent, options) {
48
- const { query, taskId } = options;
49
+ const { query, taskId, worktreeRoot } = options;
50
+ const workspaceScope = this.deriveWorkspaceScope(worktreeRoot);
49
51
  // Start search early — runs in parallel with fingerprint computation (independent operations)
50
- const searchPromise = this.searchService?.search(query, { limit: SMART_ROUTING_MAX_DOCS });
52
+ const searchPromise = this.searchService?.search(query, { limit: SMART_ROUTING_MAX_DOCS, scope: workspaceScope });
51
53
  // Prevent unhandled rejection if we return early (cache hit) while search is still pending
52
54
  searchPromise?.catch(() => { });
53
55
  // === Tier 0: Exact cache hit (0ms) ===
54
56
  let fingerprint;
55
57
  if (this.cache && this.fileSystem) {
56
- fingerprint = await this.computeContextTreeFingerprint();
58
+ fingerprint = await this.computeContextTreeFingerprint(worktreeRoot);
57
59
  const cached = this.cache.get(query, fingerprint);
58
60
  if (cached) {
59
61
  return cached + ATTRIBUTION_FOOTER;
@@ -76,7 +78,7 @@ export class QueryExecutor {
76
78
  }
77
79
  // Supplementary entity-based searches for better multi-session recall
78
80
  if (this.searchService && searchResult && searchResult.totalFound < 3) {
79
- searchResult = await this.supplementEntitySearches(query, searchResult);
81
+ searchResult = await this.supplementEntitySearches(query, searchResult, workspaceScope);
80
82
  }
81
83
  // === OOD short-circuit: no results means topic not covered ===
82
84
  if (searchResult && searchResult.results.length === 0) {
@@ -141,12 +143,18 @@ export class QueryExecutor {
141
143
  // Inject search results + metadata into the TASK session's sandbox
142
144
  agent.setSandboxVariableOnSession(taskSessionId, resultsVar, searchResult?.results ?? []);
143
145
  agent.setSandboxVariableOnSession(taskSessionId, metaVar, metadata);
146
+ // Inject workspace scope so agent follow-up searches are workspace-aware
147
+ const scopeVar = workspaceScope ? `__query_scope_${taskIdSafe}` : undefined;
148
+ if (scopeVar && workspaceScope) {
149
+ agent.setSandboxVariableOnSession(taskSessionId, scopeVar, workspaceScope);
150
+ }
144
151
  const prompt = this.buildQueryPrompt(query, {
145
152
  manifestContext,
146
153
  metadata,
147
154
  metaVar,
148
155
  prefetchedContext,
149
156
  resultsVar,
157
+ scopeVar,
150
158
  });
151
159
  // Query-optimized LLM overrides: tokens and lower temperature
152
160
  const queryOverrides = prefetchedContext
@@ -179,7 +187,12 @@ export class QueryExecutor {
179
187
  const highConfidenceResults = searchResult.results.filter((r) => r.score >= SMART_ROUTING_SCORE_THRESHOLD);
180
188
  if (highConfidenceResults.length === 0)
181
189
  return undefined;
182
- const sections = highConfidenceResults.map((r) => `### ${r.title}\n**Source**: .brv/context-tree/${r.path}\n\n${r.excerpt}`);
190
+ const sections = highConfidenceResults.map((r) => {
191
+ const source = r.origin === 'shared' && r.originAlias
192
+ ? `[${r.originAlias}]:${r.path}`
193
+ : `.brv/context-tree/${r.path}`;
194
+ return `### ${r.title}\n**Source**: ${source}\n\n${r.excerpt}`;
195
+ });
183
196
  return sections.join('\n\n---\n\n');
184
197
  }
185
198
  /**
@@ -192,9 +205,9 @@ export class QueryExecutor {
192
205
  * @param options - Prompt options with variable names and metadata
193
206
  */
194
207
  buildQueryPrompt(query, options) {
195
- const { manifestContext, metadata, metaVar, prefetchedContext, resultsVar } = options;
208
+ const { manifestContext, metadata, metaVar, prefetchedContext, resultsVar, scopeVar } = options;
196
209
  const groundingRules = `### Grounding Rules (CRITICAL)
197
- - ONLY use information from the curated knowledge base (.brv/context-tree/)
210
+ - ONLY use information from the curated knowledge base (local .brv/context-tree/ plus any read-only shared sources)
198
211
  - If no relevant knowledge is found, respond: "This topic is not covered in the knowledge base."
199
212
  - Do NOT extrapolate, infer, or generate information beyond what is explicitly stated in sources
200
213
  - Every claim MUST be traceable to a specific source file
@@ -202,11 +215,15 @@ export class QueryExecutor {
202
215
  const responseFormat = `### Response Format
203
216
  - **Summary**: Direct answer (2-3 sentences)
204
217
  - **Details**: Key findings with explanations
205
- - **Sources**: File paths from .brv/context-tree/
218
+ - **Sources**: Use .brv/context-tree/... for local knowledge and [alias]:path for shared sources
206
219
  - **Gaps**: Note any aspects not covered`;
207
220
  const manifestSection = manifestContext
208
221
  ? `\n## Structural Context (from manifest)\nThe following provides broad structural awareness of the knowledge base:\n\n${manifestContext}\n`
209
222
  : '';
223
+ // When workspace scope is active, instruct the agent to pass it to follow-up searches
224
+ const scopeGuidance = scopeVar
225
+ ? `\nFor any follow-up \`tools.searchKnowledge()\` calls, pass \`{ scope: ${scopeVar} }\` to scope results to the current workspace.`
226
+ : '';
210
227
  if (prefetchedContext) {
211
228
  return `## User Query
212
229
  ${query}
@@ -224,7 +241,7 @@ Metadata: \`${metaVar}\`
224
241
 
225
242
  Answer the user's question using the pre-fetched context above.
226
243
  If the pre-fetched context does not directly address the user's query topic, respond that the topic is not covered in the knowledge base. Do not attempt to answer from tangentially related content.
227
- If the context is insufficient but relevant, use \`code_exec\` with \`silent: true\` to read additional documents from the search results variable. Use \`setFinalResult(answer)\` when done.
244
+ If the context is insufficient but relevant, use \`code_exec\` with \`silent: true\` to read additional documents from the search results variable. Use \`setFinalResult(answer)\` when done.${scopeGuidance}
228
245
 
229
246
  ${groundingRules}
230
247
 
@@ -240,7 +257,7 @@ Metadata: \`${metaVar}\`
240
257
  ## Instructions
241
258
 
242
259
  Use \`code_exec\` to examine the search results in \`${resultsVar}\`, read relevant documents with \`tools.readFile()\`, and synthesize an answer.
243
- Use \`silent: true\` for data-loading code_exec calls. Use \`setFinalResult(answer)\` to return the final answer immediately.
260
+ Use \`silent: true\` for data-loading code_exec calls. Use \`setFinalResult(answer)\` to return the final answer immediately.${scopeGuidance}
244
261
 
245
262
  ${groundingRules}
246
263
 
@@ -250,10 +267,16 @@ ${responseFormat}`;
250
267
  * Compute a context tree fingerprint cheaply using file mtimes.
251
268
  * Used for cache invalidation — if any file in the context tree changes,
252
269
  * the fingerprint changes and cached results are invalidated.
270
+ *
271
+ * Includes worktreeRoot in the hash so different workspaces produce
272
+ * different fingerprints, preventing cross-workspace cache bleed.
253
273
  */
254
- async computeContextTreeFingerprint() {
274
+ async computeContextTreeFingerprint(worktreeRoot) {
255
275
  // Fast path: return cached fingerprint if still valid (avoids globFiles I/O)
256
- if (this.cachedFingerprint && Date.now() < this.cachedFingerprint.expiresAt) {
276
+ // Invalidate if worktreeRoot changed or knowledge source validity changed
277
+ if (this.cachedFingerprint && Date.now() < this.cachedFingerprint.expiresAt
278
+ && this.cachedFingerprint.worktreeRoot === worktreeRoot
279
+ && this.cachedFingerprint.sourceValidityHash === this.computeSourceValidityHash()) {
257
280
  return this.cachedFingerprint.value;
258
281
  }
259
282
  try {
@@ -271,8 +294,36 @@ ${responseFormat}`;
271
294
  .filter((f) => !isDerivedArtifact(f.path))
272
295
  .map((f) => ({
273
296
  mtime: f.modified?.getTime() ?? 0,
274
- path: f.path,
297
+ path: worktreeRoot ? `${worktreeRoot}:${f.path}` : f.path,
275
298
  }));
299
+ // Include shared source state in fingerprint so edits in shared
300
+ // projects invalidate cached query answers.
301
+ const loaded = this.baseDirectory ? loadSources(this.baseDirectory) : null;
302
+ if (loaded) {
303
+ // sources-file mtime detects source additions/removals
304
+ if (loaded.mtime) {
305
+ files.push({ mtime: loaded.mtime, path: '__sources.json__' });
306
+ }
307
+ // Glob each shared context tree for file-level change detection
308
+ const sharedResults = await Promise.all(loaded.origins.map(async (origin) => {
309
+ try {
310
+ const sharedGlob = await this.fileSystem.globFiles(`**/*${CONTEXT_FILE_EXTENSION}`, {
311
+ cwd: origin.contextTreeRoot,
312
+ includeMetadata: true,
313
+ maxResults: 10_000,
314
+ respectGitignore: false,
315
+ });
316
+ return sharedGlob.files
317
+ .filter((f) => !isDerivedArtifact(f.path))
318
+ .map((f) => ({ mtime: f.modified?.getTime() ?? 0, path: `shared:${origin.originKey}:${f.path}` }));
319
+ }
320
+ catch {
321
+ // Broken source — skip
322
+ return [];
323
+ }
324
+ }));
325
+ files.push(...sharedResults.flat());
326
+ }
276
327
  // Include .abstract.md siblings so newly-written abstracts invalidate the cache.
277
328
  // They are excluded by isDerivedArtifact() above, so we append them separately.
278
329
  const abstractFiles = globResult.files
@@ -284,7 +335,9 @@ ${responseFormat}`;
284
335
  const fingerprint = QueryResultCache.computeFingerprint([...files, ...abstractFiles]);
285
336
  this.cachedFingerprint = {
286
337
  expiresAt: Date.now() + QueryExecutor.FINGERPRINT_CACHE_TTL_MS,
338
+ sourceValidityHash: this.computeSourceValidityHash(),
287
339
  value: fingerprint,
340
+ worktreeRoot,
288
341
  };
289
342
  return fingerprint;
290
343
  }
@@ -292,6 +345,40 @@ ${responseFormat}`;
292
345
  return 'unknown';
293
346
  }
294
347
  }
348
+ /**
349
+ * Lightweight hash of currently valid shared source keys.
350
+ * Used by the fingerprint cache fast path to detect when a source target
351
+ * becomes broken (directory deleted) within the TTL window.
352
+ * Cost: one readFileSync + existsSync per source — sub-millisecond for typical setups.
353
+ */
354
+ computeSourceValidityHash() {
355
+ if (!this.baseDirectory)
356
+ return '';
357
+ const loaded = loadSources(this.baseDirectory);
358
+ if (!loaded)
359
+ return 'no-sources';
360
+ return loaded.origins.map((o) => o.originKey).sort().join(',');
361
+ }
362
+ /**
363
+ * Derive a workspace scope for search from the worktreeRoot.
364
+ * Returns the relative path from projectRoot to worktreeRoot,
365
+ * or undefined if they are the same (no scoping needed).
366
+ *
367
+ * KNOWN LIMITATION: Workspace scoping only works if the curated context
368
+ * tree has a subtree matching the workspace relative path (e.g., 'packages/api').
369
+ * Since the context tree is organized semantically by the LLM (topic-based),
370
+ * not by directory structure, scope filtering typically has 0 matches and
371
+ * falls through to unscoped search. A proper fix requires tagging curated
372
+ * files with source workspace metadata during curation.
373
+ */
374
+ deriveWorkspaceScope(worktreeRoot) {
375
+ if (!worktreeRoot || !this.baseDirectory)
376
+ return undefined;
377
+ if (worktreeRoot === this.baseDirectory)
378
+ return undefined;
379
+ const rel = relative(this.baseDirectory, worktreeRoot);
380
+ return rel || undefined;
381
+ }
295
382
  /**
296
383
  * Extract key entities from a query for supplementary searches.
297
384
  * Simple heuristic: split query, filter stopwords, keep significant terms.
@@ -314,20 +401,21 @@ ${responseFormat}`;
314
401
  * @param searchResult - Initial search result to supplement
315
402
  * @returns Merged search result with additional entity-based matches
316
403
  */
317
- async supplementEntitySearches(query, searchResult) {
404
+ async supplementEntitySearches(query, searchResult, scope) {
318
405
  const entities = this.extractQueryEntities(query);
319
406
  if (entities.length <= 1)
320
407
  return searchResult;
321
408
  try {
322
- const entitySearches = await Promise.allSettled(entities.slice(0, 3).map((entity) => this.searchService.search(entity, { limit: 3 })));
409
+ const entitySearches = await Promise.allSettled(entities.slice(0, 3).map((entity) => this.searchService.search(entity, { limit: 3, scope })));
323
410
  // Collect existing paths to deduplicate
324
- const existingPaths = new Set(searchResult.results.map((r) => r.path));
411
+ const existingPaths = new Set(searchResult.results.map((r) => `${r.originAlias ?? r.origin ?? 'local'}::${r.path}`));
325
412
  const supplementary = [];
326
413
  for (const settled of entitySearches) {
327
414
  if (settled.status === 'fulfilled' && settled.value.results) {
328
415
  for (const result of settled.value.results) {
329
- if (!existingPaths.has(result.path)) {
330
- existingPaths.add(result.path);
416
+ const resultKey = `${result.originAlias ?? result.origin ?? 'local'}::${result.path}`;
417
+ if (!existingPaths.has(resultKey)) {
418
+ existingPaths.add(resultKey);
331
419
  supplementary.push(result);
332
420
  }
333
421
  }
@@ -363,14 +451,20 @@ ${responseFormat}`;
363
451
  .map(async (result) => {
364
452
  let content = result.excerpt;
365
453
  try {
366
- const ctPath = join(BRV_DIR, CONTEXT_TREE_DIR, result.path);
454
+ // Use originContextTreeRoot for shared results, local context tree for local
455
+ const ctBase = result.originContextTreeRoot ?? join(BRV_DIR, CONTEXT_TREE_DIR);
456
+ const ctPath = join(ctBase, result.path);
367
457
  const { content: fullContent } = await this.fileSystem.readFile(ctPath);
368
458
  content = fullContent;
369
459
  }
370
460
  catch {
371
461
  // Use excerpt if full read fails
372
462
  }
373
- return { content, path: result.path, score: result.score, title: result.title };
463
+ // Include source attribution in path for shared results
464
+ const displayPath = result.origin === 'shared' && result.originAlias
465
+ ? `[${result.originAlias}]:${result.path}`
466
+ : result.path;
467
+ return { content, path: displayPath, score: result.score, title: result.title };
374
468
  }));
375
469
  if (!canRespondDirectly(fullResults))
376
470
  return undefined;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * SearchExecutor - Executes context tree searches via SearchKnowledgeService.
3
+ *
4
+ * Unlike QueryExecutor (Tier 0-4 with LLM synthesis), SearchExecutor is
5
+ * pure retrieval: BM25 index lookup → scored results. No LLM, no agent
6
+ * session, no sandbox, no token cost.
7
+ *
8
+ * This is the engine behind `brv search`. The CLI command and transport
9
+ * layer handle I/O; this module handles the search logic.
10
+ */
11
+ import type { ISearchKnowledgeService, SearchKnowledgeResult } from '../../../agent/infra/sandbox/tools-sdk.js';
12
+ import type { ISearchExecutor, SearchExecuteOptions } from '../../core/interfaces/executor/i-search-executor.js';
13
+ export declare class SearchExecutor implements ISearchExecutor {
14
+ private readonly searchService;
15
+ constructor(searchService: ISearchKnowledgeService);
16
+ execute(options: SearchExecuteOptions): Promise<SearchKnowledgeResult>;
17
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * SearchExecutor - Executes context tree searches via SearchKnowledgeService.
3
+ *
4
+ * Unlike QueryExecutor (Tier 0-4 with LLM synthesis), SearchExecutor is
5
+ * pure retrieval: BM25 index lookup → scored results. No LLM, no agent
6
+ * session, no sandbox, no token cost.
7
+ *
8
+ * This is the engine behind `brv search`. The CLI command and transport
9
+ * layer handle I/O; this module handles the search logic.
10
+ */
11
+ const DEFAULT_LIMIT = 10;
12
+ const MAX_LIMIT = 50;
13
+ export class SearchExecutor {
14
+ searchService;
15
+ constructor(searchService) {
16
+ this.searchService = searchService;
17
+ }
18
+ async execute(options) {
19
+ const query = options.query.trim();
20
+ if (!query) {
21
+ return { message: 'Empty query', results: [], totalFound: 0 };
22
+ }
23
+ const scope = options.scope?.trim() || undefined;
24
+ const limit = Math.min(MAX_LIMIT, Math.max(1, Math.trunc(options.limit ?? DEFAULT_LIMIT)));
25
+ return this.searchService.search(query, {
26
+ limit,
27
+ ...(scope ? { scope } : {}),
28
+ });
29
+ }
30
+ }
@@ -8,14 +8,16 @@
8
8
  */
9
9
  export type McpMode = 'global' | 'project';
10
10
  export type McpModeResult = {
11
- mode: McpMode;
12
- projectRoot?: string;
11
+ mode: 'global';
12
+ } | {
13
+ mode: 'project';
14
+ projectRoot: string;
15
+ worktreeRoot: string;
13
16
  };
14
17
  /**
15
18
  * Detects whether the MCP server is running in project or global mode.
16
19
  *
17
- * Walks up from workingDirectory looking for `.brv/config.json`.
18
- * If found, returns project mode with the discovered project root.
19
- * If the filesystem root is reached without finding it, returns global mode.
20
+ * Uses the canonical project resolver so MCP shares workspace-link semantics
21
+ * with the rest of the CLI.
20
22
  */
21
23
  export declare function detectMcpMode(workingDirectory: string): McpModeResult;
@@ -1,25 +1,18 @@
1
- import { existsSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
1
+ import { resolveProject } from '../project/resolve-project.js';
3
2
  /**
4
3
  * Detects whether the MCP server is running in project or global mode.
5
4
  *
6
- * Walks up from workingDirectory looking for `.brv/config.json`.
7
- * If found, returns project mode with the discovered project root.
8
- * If the filesystem root is reached without finding it, returns global mode.
5
+ * Uses the canonical project resolver so MCP shares workspace-link semantics
6
+ * with the rest of the CLI.
9
7
  */
10
8
  export function detectMcpMode(workingDirectory) {
11
- let current = workingDirectory;
12
- let parent = dirname(current);
13
- while (current !== parent) {
14
- if (existsSync(join(current, '.brv', 'config.json'))) {
15
- return { mode: 'project', projectRoot: current };
16
- }
17
- current = parent;
18
- parent = dirname(current);
9
+ const resolution = resolveProject({ cwd: workingDirectory });
10
+ if (!resolution) {
11
+ return { mode: 'global' };
19
12
  }
20
- // Check the root directory itself
21
- if (existsSync(join(current, '.brv', 'config.json'))) {
22
- return { mode: 'project', projectRoot: current };
23
- }
24
- return { mode: 'global' };
13
+ return {
14
+ mode: 'project',
15
+ projectRoot: resolution.projectRoot,
16
+ worktreeRoot: resolution.worktreeRoot,
17
+ };
25
18
  }
@@ -28,6 +28,7 @@ export declare class ByteRoverMcpServer {
28
28
  private reconnectorHandle;
29
29
  private readonly server;
30
30
  private transport;
31
+ private readonly worktreeRoot;
31
32
  constructor(config: McpServerConfig);
32
33
  /**
33
34
  * Starts the MCP server.
@@ -29,11 +29,13 @@ export class ByteRoverMcpServer {
29
29
  reconnectorHandle;
30
30
  server;
31
31
  transport;
32
+ worktreeRoot;
32
33
  constructor(config) {
33
34
  this.config = config;
34
- const { mode, projectRoot } = detectMcpMode(config.workingDirectory);
35
- this.mode = mode;
36
- this.projectRoot = projectRoot;
35
+ const modeResult = detectMcpMode(config.workingDirectory);
36
+ this.mode = modeResult.mode;
37
+ this.projectRoot = modeResult.mode === 'project' ? modeResult.projectRoot : undefined;
38
+ this.worktreeRoot = modeResult.mode === 'project' ? modeResult.worktreeRoot : undefined;
37
39
  this.server = new McpServer({
38
40
  name: 'byterover',
39
41
  version: config.version,
@@ -45,10 +47,13 @@ export class ByteRoverMcpServer {
45
47
  version: config.version,
46
48
  ...(this.mode === 'project' && this.projectRoot ? { projectPath: this.projectRoot } : {}),
47
49
  };
50
+ const getStartupProjectContext = () => this.mode === 'project' && this.projectRoot && this.worktreeRoot
51
+ ? { projectRoot: this.projectRoot, worktreeRoot: this.worktreeRoot }
52
+ : undefined;
48
53
  // Register tools with lazy client getter
49
54
  // Client will be set when start() is called
50
- registerBrvQueryTool(this.server, () => this.client, () => this.getWorkingDirectory());
51
- registerBrvCurateTool(this.server, () => this.client, () => this.getWorkingDirectory());
55
+ registerBrvQueryTool(this.server, () => this.client, () => this.getWorkingDirectory(), getStartupProjectContext);
56
+ registerBrvCurateTool(this.server, () => this.client, () => this.getWorkingDirectory(), getStartupProjectContext);
52
57
  }
53
58
  /**
54
59
  * Starts the MCP server.
@@ -141,7 +146,7 @@ export class ByteRoverMcpServer {
141
146
  * In global mode, returns undefined — each tool call must provide cwd.
142
147
  */
143
148
  getWorkingDirectory() {
144
- return this.mode === 'project' ? this.projectRoot : undefined;
149
+ return this.mode === 'project' ? this.worktreeRoot : undefined;
145
150
  }
146
151
  /**
147
152
  * Log to stderr (stdout is reserved for MCP protocol).
@@ -1,6 +1,7 @@
1
1
  import type { ITransportClient } from '@campfirein/brv-transport-client';
2
2
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { z } from 'zod';
4
+ import { type McpStartupProjectContext } from './mcp-project-context.js';
4
5
  export declare const BrvCurateInputSchema: z.ZodObject<{
5
6
  context: z.ZodOptional<z.ZodString>;
6
7
  cwd: z.ZodOptional<z.ZodString>;
@@ -26,4 +27,4 @@ export declare const BrvCurateInputSchema: z.ZodObject<{
26
27
  * Uses fire-and-forget pattern: returns immediately after queueing the task.
27
28
  * The curation is processed asynchronously by the ByteRover agent.
28
29
  */
29
- export declare function registerBrvCurateTool(server: McpServer, getClient: () => ITransportClient | undefined, getWorkingDirectory: () => string | undefined): void;
30
+ export declare function registerBrvCurateTool(server: McpServer, getClient: () => ITransportClient | undefined, getWorkingDirectory: () => string | undefined, getStartupProjectContext: () => McpStartupProjectContext | undefined): void;
@@ -1,8 +1,8 @@
1
1
  import { waitForConnectedClient } from '@campfirein/brv-transport-client';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { z } from 'zod';
4
- import { TransportClientEventNames, TransportTaskEventNames } from '../../../core/domain/transport/schemas.js';
5
- import { detectMcpMode } from '../mcp-mode-detector.js';
4
+ import { TransportTaskEventNames } from '../../../core/domain/transport/schemas.js';
5
+ import { associateProjectWithRetry, resolveMcpTaskContext, } from './mcp-project-context.js';
6
6
  import { resolveClientCwd } from './resolve-client-cwd.js';
7
7
  export const BrvCurateInputSchema = z.object({
8
8
  context: z
@@ -34,7 +34,7 @@ export const BrvCurateInputSchema = z.object({
34
34
  * Uses fire-and-forget pattern: returns immediately after queueing the task.
35
35
  * The curation is processed asynchronously by the ByteRover agent.
36
36
  */
37
- export function registerBrvCurateTool(server, getClient, getWorkingDirectory) {
37
+ export function registerBrvCurateTool(server, getClient, getWorkingDirectory, getStartupProjectContext) {
38
38
  server.registerTool('brv-curate', {
39
39
  description: 'Store context to the ByteRover context tree. Save patterns, decisions, or insights. ' +
40
40
  'Curation is processed asynchronously — the tool returns immediately after queueing.',
@@ -69,20 +69,11 @@ export function registerBrvCurateTool(server, getClient, getWorkingDirectory) {
69
69
  isError: true,
70
70
  };
71
71
  }
72
- // In global mode, associate client with the walked-up project root.
73
- // Walk up from clientCwd to find .brv/config.json — raw cwd may be a subdirectory.
74
- // Fire-and-forget: server handler is idempotent (first association wins).
75
- if (!getWorkingDirectory()) {
76
- const { projectRoot } = detectMcpMode(cwdResult.clientCwd);
77
- if (projectRoot) {
78
- client
79
- .requestWithAck(TransportClientEventNames.ASSOCIATE_PROJECT, {
80
- projectPath: projectRoot,
81
- })
82
- .catch(() => { });
83
- }
84
- }
85
72
  try {
73
+ const taskContext = resolveMcpTaskContext(cwdResult.clientCwd, getStartupProjectContext());
74
+ if (!getWorkingDirectory()) {
75
+ await associateProjectWithRetry(client, taskContext.projectRoot);
76
+ }
86
77
  const taskId = randomUUID();
87
78
  // Create task via transport (same pattern as brv curate command)
88
79
  // Use provided context, or empty string for file-only/folder-only mode
@@ -93,8 +84,10 @@ export function registerBrvCurateTool(server, getClient, getWorkingDirectory) {
93
84
  const ack = await client.requestWithAck(TransportTaskEventNames.CREATE, {
94
85
  clientCwd: cwdResult.clientCwd,
95
86
  content: resolvedContent,
87
+ projectPath: taskContext.projectRoot,
96
88
  taskId,
97
89
  type: taskType,
90
+ worktreeRoot: taskContext.worktreeRoot,
98
91
  ...(hasFolder && folder ? { folderPath: folder } : {}),
99
92
  ...(!hasFolder && files?.length ? { files } : {}),
100
93
  });
@@ -1,6 +1,7 @@
1
1
  import type { ITransportClient } from '@campfirein/brv-transport-client';
2
2
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { z } from 'zod';
4
+ import { type McpStartupProjectContext } from './mcp-project-context.js';
4
5
  export declare const BrvQueryInputSchema: z.ZodObject<{
5
6
  cwd: z.ZodOptional<z.ZodString>;
6
7
  query: z.ZodString;
@@ -17,4 +18,4 @@ export declare const BrvQueryInputSchema: z.ZodObject<{
17
18
  * This tool allows coding agents to query the ByteRover context tree
18
19
  * for patterns, decisions, implementation details, or any stored knowledge.
19
20
  */
20
- export declare function registerBrvQueryTool(server: McpServer, getClient: () => ITransportClient | undefined, getWorkingDirectory: () => string | undefined): void;
21
+ export declare function registerBrvQueryTool(server: McpServer, getClient: () => ITransportClient | undefined, getWorkingDirectory: () => string | undefined, getStartupProjectContext: () => McpStartupProjectContext | undefined): void;
@@ -1,8 +1,8 @@
1
1
  import { waitForConnectedClient } from '@campfirein/brv-transport-client';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { z } from 'zod';
4
- import { TransportClientEventNames, TransportTaskEventNames } from '../../../core/domain/transport/schemas.js';
5
- import { detectMcpMode } from '../mcp-mode-detector.js';
4
+ import { TransportTaskEventNames } from '../../../core/domain/transport/schemas.js';
5
+ import { associateProjectWithRetry, resolveMcpTaskContext, } from './mcp-project-context.js';
6
6
  import { resolveClientCwd } from './resolve-client-cwd.js';
7
7
  import { waitForTaskResult } from './task-result-waiter.js';
8
8
  export const BrvQueryInputSchema = z.object({
@@ -20,7 +20,7 @@ export const BrvQueryInputSchema = z.object({
20
20
  * This tool allows coding agents to query the ByteRover context tree
21
21
  * for patterns, decisions, implementation details, or any stored knowledge.
22
22
  */
23
- export function registerBrvQueryTool(server, getClient, getWorkingDirectory) {
23
+ export function registerBrvQueryTool(server, getClient, getWorkingDirectory, getStartupProjectContext) {
24
24
  server.registerTool('brv-query', {
25
25
  description: 'Query the ByteRover context tree for patterns, decisions, or implementation details.',
26
26
  inputSchema: BrvQueryInputSchema,
@@ -47,20 +47,11 @@ export function registerBrvQueryTool(server, getClient, getWorkingDirectory) {
47
47
  isError: true,
48
48
  };
49
49
  }
50
- // In global mode, associate client with the walked-up project root.
51
- // Walk up from clientCwd to find .brv/config.json — raw cwd may be a subdirectory.
52
- // Fire-and-forget: server handler is idempotent (first association wins).
53
- if (!getWorkingDirectory()) {
54
- const { projectRoot } = detectMcpMode(cwdResult.clientCwd);
55
- if (projectRoot) {
56
- client
57
- .requestWithAck(TransportClientEventNames.ASSOCIATE_PROJECT, {
58
- projectPath: projectRoot,
59
- })
60
- .catch(() => { });
61
- }
62
- }
63
50
  try {
51
+ const taskContext = resolveMcpTaskContext(cwdResult.clientCwd, getStartupProjectContext());
52
+ if (!getWorkingDirectory()) {
53
+ await associateProjectWithRetry(client, taskContext.projectRoot);
54
+ }
64
55
  const taskId = randomUUID();
65
56
  // Register event listeners BEFORE sending task:create to avoid race conditions.
66
57
  // If the task completes before listeners are set up, the task:completed event is missed.
@@ -69,8 +60,10 @@ export function registerBrvQueryTool(server, getClient, getWorkingDirectory) {
69
60
  await client.requestWithAck(TransportTaskEventNames.CREATE, {
70
61
  clientCwd: cwdResult.clientCwd,
71
62
  content: query,
63
+ projectPath: taskContext.projectRoot,
72
64
  taskId,
73
65
  type: 'query',
66
+ worktreeRoot: taskContext.worktreeRoot,
74
67
  });
75
68
  // Wait for the already-listening result promise
76
69
  const result = await resultPromise;
@@ -0,0 +1,11 @@
1
+ import type { ITransportClient } from '@campfirein/brv-transport-client';
2
+ export type McpStartupProjectContext = {
3
+ projectRoot: string;
4
+ worktreeRoot: string;
5
+ };
6
+ export type ResolvedMcpTaskContext = {
7
+ projectRoot: string;
8
+ worktreeRoot: string;
9
+ };
10
+ export declare function associateProjectWithRetry(client: ITransportClient, projectPath: string): Promise<void>;
11
+ export declare function resolveMcpTaskContext(clientCwd: string, startupProjectContext?: McpStartupProjectContext): ResolvedMcpTaskContext;
@@ -0,0 +1,54 @@
1
+ import { MCP_ASSOCIATE_PROJECT_MAX_ATTEMPTS, MCP_ASSOCIATE_PROJECT_TIMEOUT_MS } from '../../../constants.js';
2
+ import { TransportClientEventNames } from '../../../core/domain/transport/schemas.js';
3
+ import { resolveProject } from '../../project/resolve-project.js';
4
+ function describeError(error) {
5
+ return error instanceof Error ? error.message : String(error);
6
+ }
7
+ async function withTimeout(promise, timeoutMs, message) {
8
+ let timeoutId;
9
+ try {
10
+ return await Promise.race([
11
+ promise,
12
+ new Promise((_, reject) => {
13
+ timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs);
14
+ }),
15
+ ]);
16
+ }
17
+ finally {
18
+ if (timeoutId) {
19
+ clearTimeout(timeoutId);
20
+ }
21
+ }
22
+ }
23
+ export async function associateProjectWithRetry(client, projectPath) {
24
+ let lastError;
25
+ /* eslint-disable no-await-in-loop -- intentional sequential retry loop */
26
+ for (let attempt = 1; attempt <= MCP_ASSOCIATE_PROJECT_MAX_ATTEMPTS; attempt++) {
27
+ try {
28
+ await withTimeout(client.requestWithAck(TransportClientEventNames.ASSOCIATE_PROJECT, { projectPath }), MCP_ASSOCIATE_PROJECT_TIMEOUT_MS, `Timed out waiting for project association ack after ${MCP_ASSOCIATE_PROJECT_TIMEOUT_MS}ms`);
29
+ return;
30
+ }
31
+ catch (error) {
32
+ lastError = error;
33
+ }
34
+ }
35
+ /* eslint-enable no-await-in-loop */
36
+ throw new Error(`Failed to associate MCP client with project "${projectPath}": ${describeError(lastError)}. ` +
37
+ `Retry the tool call or run 'brv restart' if the daemon is unresponsive.`);
38
+ }
39
+ export function resolveMcpTaskContext(clientCwd, startupProjectContext) {
40
+ const resolution = resolveProject({ cwd: clientCwd });
41
+ if (resolution) {
42
+ return projectResolutionToTaskContext(resolution);
43
+ }
44
+ if (startupProjectContext && clientCwd === startupProjectContext.worktreeRoot) {
45
+ return startupProjectContext;
46
+ }
47
+ throw new Error(`No ByteRover project could be resolved from cwd "${clientCwd}".`);
48
+ }
49
+ function projectResolutionToTaskContext(resolution) {
50
+ return {
51
+ projectRoot: resolution.projectRoot,
52
+ worktreeRoot: resolution.worktreeRoot,
53
+ };
54
+ }