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
@@ -18,4 +18,6 @@ export type TaskInfo = {
18
18
  projectPath?: string;
19
19
  taskId: string;
20
20
  type: TaskType;
21
+ /** Workspace root (linked subdir or projectRoot if unlinked) */
22
+ worktreeRoot?: string;
21
23
  };
@@ -114,4 +114,17 @@ export interface IClientManager {
114
114
  * @param clientId - The client's Socket.IO ID
115
115
  */
116
116
  unregister(clientId: string): void;
117
+ /**
118
+ * Update a client's project path, even if already associated.
119
+ * Used for reassociation after worktree add/remove operations.
120
+ * Moves the client from the old project index to the new one,
121
+ * and fires onProjectEmpty if the old project has no remaining external clients.
122
+ *
123
+ * No-op if client is unknown.
124
+ *
125
+ * @param clientId - The client's Socket.IO ID
126
+ * @param newProjectPath - The new project path to associate
127
+ * @returns The previous project path (undefined if client not found or not previously associated)
128
+ */
129
+ updateProjectPath(clientId: string, newProjectPath: string): string | undefined;
117
130
  }
@@ -10,8 +10,12 @@ export interface CurateExecuteOptions {
10
10
  content: string;
11
11
  /** Optional file paths for --files flag */
12
12
  files?: string[];
13
+ /** Canonical project root where .brv/ lives (for post-processing: snapshot, summary, manifest) */
14
+ projectRoot?: string;
13
15
  /** Task ID for event routing (required for concurrent task isolation) */
14
16
  taskId: string;
17
+ /** Workspace root — linked subdir or same as projectRoot for direct projects */
18
+ worktreeRoot?: string;
15
19
  }
16
20
  /**
17
21
  * ICurateExecutor - Executes curate tasks with an injected CipherAgent.
@@ -4,14 +4,18 @@ import type { ICipherAgent } from '../../../../agent/core/interfaces/i-cipher-ag
4
4
  * Agent uses its default session (Single-Session pattern).
5
5
  */
6
6
  export interface FolderPackExecuteOptions {
7
- /** Client's working directory for resolving relative paths */
7
+ /** Client's working directory for resolving relative paths (shell semantics) */
8
8
  clientCwd?: string;
9
9
  /** Optional context to guide the analysis */
10
10
  content?: string;
11
- /** Folder path to pack (relative to clientCwd or absolute) */
12
- folderPath: string;
11
+ /** Folder path to pack (relative to clientCwd or absolute). When absent, defaults to worktreeRoot. */
12
+ folderPath?: string;
13
+ /** Canonical project root where .brv/ lives (for temp file location) */
14
+ projectRoot?: string;
13
15
  /** Task ID for event routing (required for concurrent task isolation) */
14
16
  taskId: string;
17
+ /** Workspace root — linked subdir or same as projectRoot. Used as default folderPath when none supplied. */
18
+ worktreeRoot?: string;
15
19
  }
16
20
  /**
17
21
  * IFolderPackExecutor - Executes folder pack + curate tasks with an injected CipherAgent.
@@ -8,6 +8,8 @@ export interface QueryExecuteOptions {
8
8
  query: string;
9
9
  /** Task ID for event routing (required for concurrent task isolation) */
10
10
  taskId: string;
11
+ /** Stable workspace root for scoping search and cache isolation */
12
+ worktreeRoot?: string;
11
13
  }
12
14
  /**
13
15
  * IQueryExecutor - Executes query tasks with an injected CipherAgent.
@@ -0,0 +1,34 @@
1
+ import type { SearchKnowledgeResult } from '../../../../agent/infra/sandbox/tools-sdk.js';
2
+ /**
3
+ * Options for executing a context tree search.
4
+ * Search is stateless — no agent session, no LLM, no task isolation needed.
5
+ */
6
+ export interface SearchExecuteOptions {
7
+ /** Maximum number of results to return (default: 10) */
8
+ limit?: number;
9
+ /** Search query */
10
+ query: string;
11
+ /** Path prefix to scope results (e.g. "auth/" for auth domain only) */
12
+ scope?: string;
13
+ }
14
+ /**
15
+ * ISearchExecutor - Executes search against the context tree's BM25 index.
16
+ *
17
+ * Unlike QueryExecutor (which requires a CipherAgent for LLM synthesis),
18
+ * SearchExecutor is stateless and returns raw search results directly.
19
+ * No agent session, no sandbox, no LLM call.
20
+ *
21
+ * Architecture:
22
+ * - SearchKnowledgeService provides BM25-indexed search
23
+ * - Executor wraps it with option validation
24
+ * - Results are SearchKnowledgeResult (paths, scores, excerpts)
25
+ */
26
+ export interface ISearchExecutor {
27
+ /**
28
+ * Execute a context tree search.
29
+ *
30
+ * @param options - Search options (query, limit, scope)
31
+ * @returns Raw search results with paths, scores, and excerpts
32
+ */
33
+ execute(options: SearchExecuteOptions): Promise<SearchKnowledgeResult>;
34
+ }
@@ -1,3 +1,4 @@
1
1
  export * from './i-curate-executor.js';
2
2
  export * from './i-folder-pack-executor.js';
3
3
  export * from './i-query-executor.js';
4
+ export * from './i-search-executor.js';
@@ -1,3 +1,4 @@
1
1
  export * from './i-curate-executor.js';
2
2
  export * from './i-folder-pack-executor.js';
3
3
  export * from './i-query-executor.js';
4
+ export * from './i-search-executor.js';
@@ -41,6 +41,7 @@ export declare class ClientManager implements IClientManager {
41
41
  register(clientId: string, type: ClientType, projectPath?: string): void;
42
42
  setAgentName(clientId: string, agentName: string): void;
43
43
  unregister(clientId: string): void;
44
+ updateProjectPath(clientId: string, newProjectPath: string): string | undefined;
44
45
  private addToProjectIndex;
45
46
  /**
46
47
  * Check if a project has no remaining external clients.
@@ -111,6 +111,22 @@ export class ClientManager {
111
111
  // Notify idle timeout policy
112
112
  this.clientDisconnectedCallback?.();
113
113
  }
114
+ updateProjectPath(clientId, newProjectPath) {
115
+ const client = this.clients.get(clientId);
116
+ if (!client)
117
+ return undefined;
118
+ const oldPath = client.updateProjectPath(newProjectPath);
119
+ // Move between project indexes
120
+ if (oldPath) {
121
+ this.removeFromProjectIndex(clientId, oldPath);
122
+ }
123
+ this.addToProjectIndex(clientId, newProjectPath);
124
+ // Check if old project is now empty
125
+ if (oldPath && oldPath !== newProjectPath && client.isExternalClient) {
126
+ this.checkProjectEmpty(oldPath);
127
+ }
128
+ return oldPath;
129
+ }
114
130
  addToProjectIndex(clientId, projectPath) {
115
131
  let members = this.projectClients.get(projectPath);
116
132
  if (!members) {
@@ -28,13 +28,16 @@ import { FolderPackService } from '../../../agent/infra/folder-pack/folder-pack-
28
28
  import { SessionMetadataStore } from '../../../agent/infra/session/session-metadata-store.js';
29
29
  import { createSearchKnowledgeService } from '../../../agent/infra/tools/implementations/search-knowledge-service.js';
30
30
  import { AuthEvents } from '../../../shared/transport/events/auth-events.js';
31
+ import { decodeSearchContent } from '../../../shared/transport/search-content.js';
31
32
  import { getCurrentConfig } from '../../config/environment.js';
32
33
  import { DEFAULT_LLM_MODEL, PROJECT } from '../../constants.js';
33
34
  import { serializeTaskError, TaskError, TaskErrorCode } from '../../core/domain/errors/task-error.js';
35
+ import { loadSources } from '../../core/domain/source/source-schema.js';
34
36
  import { TransportAgentEventNames, TransportDaemonEventNames, TransportStateEventNames, TransportTaskEventNames, } from '../../core/domain/transport/schemas.js';
35
37
  import { CurateExecutor } from '../executor/curate-executor.js';
36
38
  import { FolderPackExecutor } from '../executor/folder-pack-executor.js';
37
39
  import { QueryExecutor } from '../executor/query-executor.js';
40
+ import { SearchExecutor } from '../executor/search-executor.js';
38
41
  import { AgentInstanceDiscovery } from '../transport/agent-instance-discovery.js';
39
42
  import { createAgentLogger } from './agent-logger.js';
40
43
  import { resolveSessionId } from './session-resolver.js';
@@ -189,10 +192,14 @@ async function start() {
189
192
  cachedProviderHeaders = providerResult.providerHeaders ? JSON.stringify(providerResult.providerHeaders) : undefined;
190
193
  agentLog(`Provider: ${activeProvider}, Model: ${activeModel ?? 'default'}`);
191
194
  // 5. Create CipherAgent with lazy providers + transport client
195
+ // Load knowledge sources early so shared context tree roots can be shared with both
196
+ // the agent's FileSystemService (via config) and the executor's FileSystemService
197
+ const sourcesData = loadSources(projectPath);
198
+ const sharedAllowedPaths = (sourcesData?.origins ?? []).map((o) => o.contextTreeRoot);
192
199
  const envConfig = getCurrentConfig();
193
200
  const agentConfig = {
194
201
  apiBaseUrl: envConfig.llmApiBaseUrl,
195
- fileSystem: { workingDirectory: projectPath },
202
+ fileSystem: { allowedPaths: ['.', ...sharedAllowedPaths], workingDirectory: projectPath },
196
203
  llm: {
197
204
  maxIterations: 10,
198
205
  maxTokens: 4096,
@@ -279,7 +286,10 @@ async function start() {
279
286
  }
280
287
  });
281
288
  // 6. Create FileSystemService + SearchKnowledgeService for smart query routing
282
- const fileSystemService = new FileSystemService({ workingDirectory: projectPath });
289
+ const fileSystemService = new FileSystemService({
290
+ allowedPaths: ['.', ...sharedAllowedPaths],
291
+ workingDirectory: projectPath,
292
+ });
283
293
  await fileSystemService.initialize();
284
294
  const searchService = createSearchKnowledgeService(fileSystemService, { baseDirectory: projectPath });
285
295
  // 7. Create executors and listen for task:execute from pool
@@ -293,10 +303,11 @@ async function start() {
293
303
  fileSystem: fileSystemService,
294
304
  searchService,
295
305
  });
306
+ const searchExecutor = new SearchExecutor(searchService);
296
307
  transport.on(TransportTaskEventNames.EXECUTE, (task) => {
297
308
  agentLog(`task:execute received taskId=${task.taskId} type=${task.type} activeTaskCount=${activeTaskCount + 1}`);
298
309
  // eslint-disable-next-line no-void
299
- void executeTask(task, curateExecutor, folderPackExecutor, queryExecutor);
310
+ void executeTask(task, curateExecutor, folderPackExecutor, queryExecutor, searchExecutor);
300
311
  });
301
312
  // 8. Register with transport server (for TransportHandlers tracking)
302
313
  await transport.requestWithAck('agent:register', { projectPath });
@@ -304,15 +315,19 @@ async function start() {
304
315
  process.send?.({ clientId, type: 'ready' });
305
316
  agentLog('Ready — listening for tasks');
306
317
  }
307
- async function executeTask(task, curateExecutor, folderPackExecutor, queryExecutor) {
308
- const { clientCwd, clientId, content, files, folderPath, taskId, type } = task;
318
+ async function executeTask(task, curateExecutor, folderPackExecutor, queryExecutor, searchExecutor) {
319
+ const { clientCwd, clientId, content, files, folderPath, taskId, type, worktreeRoot } = task;
309
320
  if (!transport || !agent)
310
321
  return;
311
- const freshProviderConfig = await transport.requestWithAck(TransportStateEventNames.GET_PROVIDER_CONFIG);
312
- const validationError = validateProviderForTask(freshProviderConfig);
313
- if (validationError) {
314
- transport.request(TransportTaskEventNames.ERROR, { clientId, error: validationError, taskId });
315
- return;
322
+ // Search tasks are pure BM25 retrieval — no LLM, no provider needed.
323
+ // Skip provider validation so search works even without a configured provider.
324
+ if (type !== 'search') {
325
+ const freshProviderConfig = await transport.requestWithAck(TransportStateEventNames.GET_PROVIDER_CONFIG);
326
+ const validationError = validateProviderForTask(freshProviderConfig);
327
+ if (validationError) {
328
+ transport.request(TransportTaskEventNames.ERROR, { clientId, error: validationError, taskId });
329
+ return;
330
+ }
316
331
  }
317
332
  activeTaskCount++;
318
333
  try {
@@ -372,7 +387,7 @@ async function executeTask(task, curateExecutor, folderPackExecutor, queryExecut
372
387
  let result;
373
388
  switch (type) {
374
389
  case 'curate': {
375
- result = await curateExecutor.executeWithAgent(agent, { clientCwd, content, files, taskId });
390
+ result = await curateExecutor.executeWithAgent(agent, { clientCwd, content, files, projectRoot: projectPath, taskId, worktreeRoot });
376
391
  break;
377
392
  }
378
393
  case 'curate-folder': {
@@ -380,12 +395,20 @@ async function executeTask(task, curateExecutor, folderPackExecutor, queryExecut
380
395
  clientCwd,
381
396
  content,
382
397
  folderPath: folderPath,
398
+ projectRoot: projectPath,
383
399
  taskId,
400
+ worktreeRoot,
384
401
  });
385
402
  break;
386
403
  }
387
404
  case 'query': {
388
- result = await queryExecutor.executeWithAgent(agent, { query: content, taskId });
405
+ result = await queryExecutor.executeWithAgent(agent, { query: content, taskId, worktreeRoot });
406
+ break;
407
+ }
408
+ case 'search': {
409
+ const searchOptions = decodeSearchContent(content);
410
+ const searchResult = await searchExecutor.execute(searchOptions);
411
+ result = JSON.stringify(searchResult);
389
412
  break;
390
413
  }
391
414
  }
@@ -36,7 +36,7 @@ export class CurateExecutor {
36
36
  this.fileContentReader = fileContentReader ?? createFileContentReader();
37
37
  }
38
38
  async executeWithAgent(agent, options) {
39
- const { clientCwd, content, files, taskId } = options;
39
+ const { clientCwd, content, files, projectRoot, taskId } = options;
40
40
  // --- Phase 1: Preprocessing (no sessions created yet — safe to throw) ---
41
41
  const fileReferenceInstructions = await this.processFileReferences(files ?? [], clientCwd);
42
42
  const fullContext = fileReferenceInstructions ? `${content}\n${fileReferenceInstructions}` : content;
@@ -45,7 +45,9 @@ export class CurateExecutor {
45
45
  const effectiveContext = compactionResult.context;
46
46
  // --- Phase 3: Curation (session created AFTER preprocessing + compaction) ---
47
47
  // Capture pre-curation state for snapshot diff (summary propagation)
48
- const baseDir = clientCwd ?? process.cwd();
48
+ // Post-processing (snapshot, summary, manifest) operates on projectRoot where .brv/ lives.
49
+ // worktreeRoot is a linked subdir — .brv/ does not exist there in linked setups.
50
+ const baseDir = projectRoot ?? clientCwd ?? process.cwd();
49
51
  const snapshotService = new FileContextTreeSnapshotService({ baseDirectory: baseDir });
50
52
  let preState;
51
53
  try {
@@ -58,7 +58,11 @@ export function formatDirectResponse(query, results) {
58
58
  return `### ${r.title}\n\n${truncatedContent}`;
59
59
  })
60
60
  .join('\n\n---\n\n');
61
- const sources = topResults.map((r) => `- \`.brv/context-tree/${r.path}\``).join('\n');
61
+ const sources = topResults.map((r) => {
62
+ // Paths starting with [ are already namespaced (linked results)
63
+ const displayPath = r.path.startsWith('[') ? r.path : `.brv/context-tree/${r.path}`;
64
+ return `- \`${displayPath}\``;
65
+ }).join('\n');
62
66
  return `**Summary**: ${summary}
63
67
 
64
68
  **Details**:
@@ -38,17 +38,28 @@ export class FolderPackExecutor {
38
38
  this.folderPackService = folderPackService;
39
39
  }
40
40
  async executeWithAgent(agent, options) {
41
- const { clientCwd, content, folderPath, taskId } = options;
41
+ const { clientCwd, content, folderPath, projectRoot, taskId, worktreeRoot } = options;
42
+ // Resolve folder path:
43
+ // - Absent folderPath → default to worktreeRoot (implicit workspace default)
44
+ // - Relative folderPath → resolve from clientCwd (shell semantics)
45
+ // - Absolute folderPath → use as-is
46
+ let absoluteFolderPath;
42
47
  if (!folderPath) {
43
- throw new Error('folderPath is required for curate-folder tasks');
48
+ absoluteFolderPath = worktreeRoot ?? clientCwd ?? process.cwd();
44
49
  }
45
- // Resolve folder path
46
- const basePath = clientCwd ?? process.cwd();
47
- const absoluteFolderPath = path.isAbsolute(folderPath) ? folderPath : path.resolve(basePath, folderPath);
48
- const snapshotService = new FileContextTreeSnapshotService({ baseDirectory: basePath });
50
+ else if (path.isAbsolute(folderPath)) {
51
+ absoluteFolderPath = folderPath;
52
+ }
53
+ else {
54
+ const shellCwd = clientCwd ?? process.cwd();
55
+ absoluteFolderPath = path.resolve(shellCwd, folderPath);
56
+ }
57
+ // Temp file location: use projectRoot where .brv/ lives (accessible to sandbox)
58
+ const tempFileDir = projectRoot ?? clientCwd ?? process.cwd();
59
+ const snapshotService = new FileContextTreeSnapshotService({ baseDirectory: tempFileDir });
49
60
  let preState;
50
61
  try {
51
- preState = await snapshotService.getCurrentState(basePath);
62
+ preState = await snapshotService.getCurrentState(tempFileDir);
52
63
  }
53
64
  catch {
54
65
  // Fail-open: if snapshot fails, skip summary propagation
@@ -62,17 +73,17 @@ export class FolderPackExecutor {
62
73
  // Use iterative extraction strategy (inspired by rlm)
63
74
  // Stores packed folder in sandbox environment and lets agent iteratively query/extract
64
75
  // This avoids token limits entirely - works for folders of any size
65
- const response = await this.executeIterative(agent, packResult, content, absoluteFolderPath, taskId, basePath);
76
+ const response = await this.executeIterative(agent, packResult, content, absoluteFolderPath, taskId, tempFileDir);
66
77
  if (preState) {
67
78
  try {
68
- const postState = await snapshotService.getCurrentState(basePath);
79
+ const postState = await snapshotService.getCurrentState(tempFileDir);
69
80
  const changedPaths = diffStates(preState, postState);
70
81
  if (changedPaths.length > 0) {
71
82
  const summaryService = new FileContextTreeSummaryService();
72
- const results = await summaryService.propagateStaleness(changedPaths, agent, basePath);
83
+ const results = await summaryService.propagateStaleness(changedPaths, agent, tempFileDir);
73
84
  if (results.some((result) => result.actionTaken)) {
74
- const manifestService = new FileContextTreeManifestService({ baseDirectory: basePath });
75
- await manifestService.buildManifest(basePath);
85
+ const manifestService = new FileContextTreeManifestService({ baseDirectory: tempFileDir });
86
+ await manifestService.buildManifest(tempFileDir);
76
87
  }
77
88
  }
78
89
  }
@@ -64,8 +64,31 @@ export declare class QueryExecutor implements IQueryExecutor {
64
64
  * Compute a context tree fingerprint cheaply using file mtimes.
65
65
  * Used for cache invalidation — if any file in the context tree changes,
66
66
  * the fingerprint changes and cached results are invalidated.
67
+ *
68
+ * Includes worktreeRoot in the hash so different workspaces produce
69
+ * different fingerprints, preventing cross-workspace cache bleed.
67
70
  */
68
71
  private computeContextTreeFingerprint;
72
+ /**
73
+ * Lightweight hash of currently valid shared source keys.
74
+ * Used by the fingerprint cache fast path to detect when a source target
75
+ * becomes broken (directory deleted) within the TTL window.
76
+ * Cost: one readFileSync + existsSync per source — sub-millisecond for typical setups.
77
+ */
78
+ private computeSourceValidityHash;
79
+ /**
80
+ * Derive a workspace scope for search from the worktreeRoot.
81
+ * Returns the relative path from projectRoot to worktreeRoot,
82
+ * or undefined if they are the same (no scoping needed).
83
+ *
84
+ * KNOWN LIMITATION: Workspace scoping only works if the curated context
85
+ * tree has a subtree matching the workspace relative path (e.g., 'packages/api').
86
+ * Since the context tree is organized semantically by the LLM (topic-based),
87
+ * not by directory structure, scope filtering typically has 0 matches and
88
+ * falls through to unscoped search. A proper fix requires tagging curated
89
+ * files with source workspace metadata during curation.
90
+ */
91
+ private deriveWorkspaceScope;
69
92
  /**
70
93
  * Extract key entities from a query for supplementary searches.
71
94
  * Simple heuristic: split query, filter stopwords, keep significant terms.