byterover-cli 3.0.1 → 3.2.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 (196) hide show
  1. package/.env.production +4 -0
  2. package/README.md +17 -0
  3. package/dist/agent/core/domain/tools/constants.d.ts +1 -0
  4. package/dist/agent/core/domain/tools/constants.js +1 -0
  5. package/dist/agent/core/interfaces/cipher-services.d.ts +8 -0
  6. package/dist/agent/core/interfaces/i-cipher-agent.d.ts +1 -0
  7. package/dist/agent/infra/agent/agent-error-codes.d.ts +0 -1
  8. package/dist/agent/infra/agent/agent-error-codes.js +0 -1
  9. package/dist/agent/infra/agent/agent-error.d.ts +0 -1
  10. package/dist/agent/infra/agent/agent-error.js +0 -1
  11. package/dist/agent/infra/agent/agent-schemas.d.ts +8 -0
  12. package/dist/agent/infra/agent/agent-schemas.js +1 -0
  13. package/dist/agent/infra/agent/agent-state-manager.d.ts +1 -3
  14. package/dist/agent/infra/agent/agent-state-manager.js +1 -3
  15. package/dist/agent/infra/agent/base-agent.d.ts +1 -1
  16. package/dist/agent/infra/agent/base-agent.js +1 -1
  17. package/dist/agent/infra/agent/cipher-agent.d.ts +15 -1
  18. package/dist/agent/infra/agent/cipher-agent.js +188 -3
  19. package/dist/agent/infra/agent/index.d.ts +1 -1
  20. package/dist/agent/infra/agent/index.js +1 -1
  21. package/dist/agent/infra/agent/service-initializer.d.ts +3 -3
  22. package/dist/agent/infra/agent/service-initializer.js +14 -8
  23. package/dist/agent/infra/agent/types.d.ts +0 -1
  24. package/dist/agent/infra/file-system/file-system-service.js +6 -5
  25. package/dist/agent/infra/folder-pack/folder-pack-service.d.ts +1 -0
  26. package/dist/agent/infra/folder-pack/folder-pack-service.js +29 -15
  27. package/dist/agent/infra/llm/providers/openai.js +12 -0
  28. package/dist/agent/infra/llm/stream-to-text.d.ts +7 -0
  29. package/dist/agent/infra/llm/stream-to-text.js +14 -0
  30. package/dist/agent/infra/map/abstract-generator.d.ts +22 -0
  31. package/dist/agent/infra/map/abstract-generator.js +67 -0
  32. package/dist/agent/infra/map/abstract-queue.d.ts +67 -0
  33. package/dist/agent/infra/map/abstract-queue.js +218 -0
  34. package/dist/agent/infra/memory/memory-deduplicator.d.ts +44 -0
  35. package/dist/agent/infra/memory/memory-deduplicator.js +88 -0
  36. package/dist/agent/infra/memory/memory-manager.d.ts +1 -0
  37. package/dist/agent/infra/memory/memory-manager.js +6 -5
  38. package/dist/agent/infra/sandbox/curate-service.d.ts +4 -2
  39. package/dist/agent/infra/sandbox/curate-service.js +20 -7
  40. package/dist/agent/infra/sandbox/local-sandbox.d.ts +5 -0
  41. package/dist/agent/infra/sandbox/local-sandbox.js +57 -1
  42. package/dist/agent/infra/sandbox/sandbox-service.js +1 -0
  43. package/dist/agent/infra/sandbox/tools-sdk.d.ts +13 -1
  44. package/dist/agent/infra/sandbox/tools-sdk.js +9 -1
  45. package/dist/agent/infra/session/session-compressor.d.ts +43 -0
  46. package/dist/agent/infra/session/session-compressor.js +296 -0
  47. package/dist/agent/infra/session/session-manager.d.ts +7 -0
  48. package/dist/agent/infra/session/session-manager.js +9 -0
  49. package/dist/agent/infra/tools/implementations/curate-tool.d.ts +3 -2
  50. package/dist/agent/infra/tools/implementations/curate-tool.js +54 -27
  51. package/dist/agent/infra/tools/implementations/expand-knowledge-tool.d.ts +3 -3
  52. package/dist/agent/infra/tools/implementations/expand-knowledge-tool.js +34 -7
  53. package/dist/agent/infra/tools/implementations/ingest-resource-tool.d.ts +17 -0
  54. package/dist/agent/infra/tools/implementations/ingest-resource-tool.js +224 -0
  55. package/dist/agent/infra/tools/implementations/memory-symbol-tree.d.ts +8 -0
  56. package/dist/agent/infra/tools/implementations/search-knowledge-service.d.ts +1 -1
  57. package/dist/agent/infra/tools/implementations/search-knowledge-service.js +392 -106
  58. package/dist/agent/infra/tools/implementations/search-knowledge-tool.js +2 -2
  59. package/dist/agent/infra/tools/implementations/write-file-tool.d.ts +2 -1
  60. package/dist/agent/infra/tools/implementations/write-file-tool.js +16 -2
  61. package/dist/agent/infra/tools/tool-provider.js +1 -0
  62. package/dist/agent/infra/tools/tool-registry.d.ts +3 -0
  63. package/dist/agent/infra/tools/tool-registry.js +16 -5
  64. package/dist/agent/infra/tools/write-guard.d.ts +11 -0
  65. package/dist/agent/infra/tools/write-guard.js +48 -0
  66. package/dist/agent/resources/prompts/system-prompt.yml +9 -0
  67. package/dist/agent/resources/tools/expand_knowledge.txt +4 -0
  68. package/dist/agent/resources/tools/search_knowledge.txt +11 -1
  69. package/dist/oclif/commands/curate/index.js +4 -3
  70. package/dist/oclif/commands/curate/view.js +2 -2
  71. package/dist/oclif/commands/main.js +13 -0
  72. package/dist/oclif/commands/query.js +4 -3
  73. package/dist/oclif/commands/source/add.d.ts +12 -0
  74. package/dist/oclif/commands/source/add.js +42 -0
  75. package/dist/oclif/commands/source/index.d.ts +6 -0
  76. package/dist/oclif/commands/source/index.js +8 -0
  77. package/dist/oclif/commands/source/list.d.ts +6 -0
  78. package/dist/oclif/commands/source/list.js +32 -0
  79. package/dist/oclif/commands/source/remove.d.ts +9 -0
  80. package/dist/oclif/commands/source/remove.js +33 -0
  81. package/dist/oclif/commands/status.d.ts +5 -1
  82. package/dist/oclif/commands/status.js +41 -6
  83. package/dist/oclif/commands/worktree/add.d.ts +12 -0
  84. package/dist/oclif/commands/worktree/add.js +44 -0
  85. package/dist/oclif/commands/worktree/index.d.ts +6 -0
  86. package/dist/oclif/commands/worktree/index.js +8 -0
  87. package/dist/oclif/commands/worktree/list.d.ts +6 -0
  88. package/dist/oclif/commands/worktree/list.js +28 -0
  89. package/dist/oclif/commands/worktree/remove.d.ts +9 -0
  90. package/dist/oclif/commands/worktree/remove.js +35 -0
  91. package/dist/oclif/hooks/init/validate-brv-config.js +4 -0
  92. package/dist/oclif/lib/daemon-client.d.ts +4 -2
  93. package/dist/oclif/lib/daemon-client.js +19 -3
  94. package/dist/server/constants.d.ts +8 -0
  95. package/dist/server/constants.js +10 -0
  96. package/dist/server/core/domain/client/client-info.d.ts +7 -0
  97. package/dist/server/core/domain/client/client-info.js +11 -0
  98. package/dist/server/core/domain/knowledge/memory-scoring.d.ts +3 -3
  99. package/dist/server/core/domain/knowledge/memory-scoring.js +5 -5
  100. package/dist/server/core/domain/knowledge/summary-types.d.ts +4 -0
  101. package/dist/server/core/domain/project/worktrees-schema.d.ts +29 -0
  102. package/dist/server/core/domain/project/worktrees-schema.js +17 -0
  103. package/dist/server/core/domain/source/source-operations.d.ts +31 -0
  104. package/dist/server/core/domain/source/source-operations.js +201 -0
  105. package/dist/server/core/domain/source/source-schema.d.ts +94 -0
  106. package/dist/server/core/domain/source/source-schema.js +121 -0
  107. package/dist/server/core/domain/transport/schemas.d.ts +18 -10
  108. package/dist/server/core/domain/transport/schemas.js +4 -0
  109. package/dist/server/core/domain/transport/task-info.d.ts +2 -0
  110. package/dist/server/core/interfaces/client/i-client-manager.d.ts +13 -0
  111. package/dist/server/core/interfaces/executor/i-curate-executor.d.ts +4 -0
  112. package/dist/server/core/interfaces/executor/i-folder-pack-executor.d.ts +7 -3
  113. package/dist/server/core/interfaces/executor/i-query-executor.d.ts +2 -0
  114. package/dist/server/infra/client/client-manager.d.ts +1 -0
  115. package/dist/server/infra/client/client-manager.js +16 -0
  116. package/dist/server/infra/context-tree/derived-artifact.js +5 -1
  117. package/dist/server/infra/context-tree/file-context-tree-manifest-service.d.ts +2 -1
  118. package/dist/server/infra/context-tree/file-context-tree-manifest-service.js +43 -7
  119. package/dist/server/infra/context-tree/file-context-tree-summary-service.js +20 -2
  120. package/dist/server/infra/daemon/agent-process.js +15 -5
  121. package/dist/server/infra/executor/curate-executor.js +6 -3
  122. package/dist/server/infra/executor/direct-search-responder.js +5 -1
  123. package/dist/server/infra/executor/folder-pack-executor.js +88 -7
  124. package/dist/server/infra/executor/query-executor.d.ts +23 -0
  125. package/dist/server/infra/executor/query-executor.js +125 -23
  126. package/dist/server/infra/mcp/mcp-mode-detector.d.ts +7 -5
  127. package/dist/server/infra/mcp/mcp-mode-detector.js +11 -18
  128. package/dist/server/infra/mcp/mcp-server.d.ts +1 -0
  129. package/dist/server/infra/mcp/mcp-server.js +11 -6
  130. package/dist/server/infra/mcp/tools/brv-curate-tool.d.ts +2 -1
  131. package/dist/server/infra/mcp/tools/brv-curate-tool.js +9 -16
  132. package/dist/server/infra/mcp/tools/brv-query-tool.d.ts +2 -1
  133. package/dist/server/infra/mcp/tools/brv-query-tool.js +9 -16
  134. package/dist/server/infra/mcp/tools/mcp-project-context.d.ts +11 -0
  135. package/dist/server/infra/mcp/tools/mcp-project-context.js +54 -0
  136. package/dist/server/infra/process/connection-coordinator.js +11 -0
  137. package/dist/server/infra/process/feature-handlers.js +4 -1
  138. package/dist/server/infra/process/task-router.d.ts +1 -0
  139. package/dist/server/infra/process/task-router.js +60 -5
  140. package/dist/server/infra/project/resolve-project.d.ts +106 -0
  141. package/dist/server/infra/project/resolve-project.js +473 -0
  142. package/dist/server/infra/transport/handlers/index.d.ts +4 -0
  143. package/dist/server/infra/transport/handlers/index.js +2 -0
  144. package/dist/server/infra/transport/handlers/source-handler.d.ts +12 -0
  145. package/dist/server/infra/transport/handlers/source-handler.js +37 -0
  146. package/dist/server/infra/transport/handlers/status-handler.js +65 -13
  147. package/dist/server/infra/transport/handlers/worktree-handler.d.ts +12 -0
  148. package/dist/server/infra/transport/handlers/worktree-handler.js +67 -0
  149. package/dist/server/infra/transport/transport-connector.d.ts +10 -4
  150. package/dist/server/infra/transport/transport-connector.js +2 -2
  151. package/dist/server/utils/curate-result-parser.d.ts +4 -4
  152. package/dist/server/utils/path-utils.d.ts +5 -0
  153. package/dist/server/utils/path-utils.js +11 -1
  154. package/dist/shared/transport/events/client-events.d.ts +3 -0
  155. package/dist/shared/transport/events/client-events.js +3 -0
  156. package/dist/shared/transport/events/index.d.ts +13 -0
  157. package/dist/shared/transport/events/index.js +9 -0
  158. package/dist/shared/transport/events/source-events.d.ts +30 -0
  159. package/dist/shared/transport/events/source-events.js +5 -0
  160. package/dist/shared/transport/events/status-events.d.ts +5 -0
  161. package/dist/shared/transport/events/task-events.d.ts +4 -1
  162. package/dist/shared/transport/events/worktree-events.d.ts +31 -0
  163. package/dist/shared/transport/events/worktree-events.js +5 -0
  164. package/dist/shared/transport/types/dto.d.ts +26 -0
  165. package/dist/tui/features/commands/definitions/index.js +6 -0
  166. package/dist/tui/features/commands/definitions/source-add.d.ts +2 -0
  167. package/dist/tui/features/commands/definitions/source-add.js +48 -0
  168. package/dist/tui/features/commands/definitions/source-list.d.ts +2 -0
  169. package/dist/tui/features/commands/definitions/source-list.js +47 -0
  170. package/dist/tui/features/commands/definitions/source-remove.d.ts +2 -0
  171. package/dist/tui/features/commands/definitions/source-remove.js +38 -0
  172. package/dist/tui/features/commands/definitions/source.d.ts +2 -0
  173. package/dist/tui/features/commands/definitions/source.js +8 -0
  174. package/dist/tui/features/commands/definitions/worktree-add.d.ts +2 -0
  175. package/dist/tui/features/commands/definitions/worktree-add.js +35 -0
  176. package/dist/tui/features/commands/definitions/worktree-list.d.ts +2 -0
  177. package/dist/tui/features/commands/definitions/worktree-list.js +36 -0
  178. package/dist/tui/features/commands/definitions/worktree-remove.d.ts +2 -0
  179. package/dist/tui/features/commands/definitions/worktree-remove.js +33 -0
  180. package/dist/tui/features/commands/definitions/worktree.d.ts +2 -0
  181. package/dist/tui/features/commands/definitions/worktree.js +8 -0
  182. package/dist/tui/features/curate/api/create-curate-task.js +3 -1
  183. package/dist/tui/features/query/api/create-query-task.js +3 -1
  184. package/dist/tui/features/source/api/source-api.d.ts +4 -0
  185. package/dist/tui/features/source/api/source-api.js +22 -0
  186. package/dist/tui/features/status/api/get-status.js +2 -1
  187. package/dist/tui/features/status/utils/format-status.js +23 -1
  188. package/dist/tui/features/transport/components/transport-initializer.js +36 -1
  189. package/dist/tui/features/worktree/api/worktree-api.d.ts +4 -0
  190. package/dist/tui/features/worktree/api/worktree-api.js +22 -0
  191. package/dist/tui/repl-startup.d.ts +2 -0
  192. package/dist/tui/repl-startup.js +5 -3
  193. package/dist/tui/stores/transport-store.d.ts +6 -0
  194. package/dist/tui/stores/transport-store.js +6 -0
  195. package/oclif.manifest.json +261 -1
  196. package/package.json +10 -4
@@ -1,5 +1,6 @@
1
- import { join } from 'node:path';
2
- import { BRV_DIR, CONTEXT_FILE_EXTENSION, CONTEXT_TREE_DIR } from '../../constants.js';
1
+ import { join, relative } from 'node:path';
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) {
@@ -124,7 +126,7 @@ export class QueryExecutor {
124
126
  }
125
127
  }
126
128
  // Create per-task session for parallel isolation (own sandbox + history + LLM service)
127
- const taskSessionId = await agent.createTaskSession(taskId, 'query');
129
+ const taskSessionId = await agent.createTaskSession(taskId, 'query', { userFacing: true });
128
130
  // Task-scoped variable names for sandbox injection (RLM pattern).
129
131
  // Replace hyphens with underscores: UUIDs have hyphens which are invalid in JS identifiers,
130
132
  // so the LLM uses underscores when writing code-exec calls — matching curate-executor pattern.
@@ -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,12 +294,50 @@ ${responseFormat}`;
271
294
  .filter((f) => !isDerivedArtifact(f.path))
272
295
  .map((f) => ({
273
296
  mtime: f.modified?.getTime() ?? 0,
297
+ path: worktreeRoot ? `${worktreeRoot}:${f.path}` : f.path,
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
+ }
327
+ // Include .abstract.md siblings so newly-written abstracts invalidate the cache.
328
+ // They are excluded by isDerivedArtifact() above, so we append them separately.
329
+ const abstractFiles = globResult.files
330
+ .filter((f) => f.path.endsWith(ABSTRACT_EXTENSION))
331
+ .map((f) => ({
332
+ mtime: f.modified?.getTime() ?? 0,
274
333
  path: f.path,
275
334
  }));
276
- const fingerprint = QueryResultCache.computeFingerprint(files);
335
+ const fingerprint = QueryResultCache.computeFingerprint([...files, ...abstractFiles]);
277
336
  this.cachedFingerprint = {
278
337
  expiresAt: Date.now() + QueryExecutor.FINGERPRINT_CACHE_TTL_MS,
338
+ sourceValidityHash: this.computeSourceValidityHash(),
279
339
  value: fingerprint,
340
+ worktreeRoot,
280
341
  };
281
342
  return fingerprint;
282
343
  }
@@ -284,6 +345,40 @@ ${responseFormat}`;
284
345
  return 'unknown';
285
346
  }
286
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
+ }
287
382
  /**
288
383
  * Extract key entities from a query for supplementary searches.
289
384
  * Simple heuristic: split query, filter stopwords, keep significant terms.
@@ -306,20 +401,21 @@ ${responseFormat}`;
306
401
  * @param searchResult - Initial search result to supplement
307
402
  * @returns Merged search result with additional entity-based matches
308
403
  */
309
- async supplementEntitySearches(query, searchResult) {
404
+ async supplementEntitySearches(query, searchResult, scope) {
310
405
  const entities = this.extractQueryEntities(query);
311
406
  if (entities.length <= 1)
312
407
  return searchResult;
313
408
  try {
314
- 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 })));
315
410
  // Collect existing paths to deduplicate
316
- 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}`));
317
412
  const supplementary = [];
318
413
  for (const settled of entitySearches) {
319
414
  if (settled.status === 'fulfilled' && settled.value.results) {
320
415
  for (const result of settled.value.results) {
321
- if (!existingPaths.has(result.path)) {
322
- existingPaths.add(result.path);
416
+ const resultKey = `${result.originAlias ?? result.origin ?? 'local'}::${result.path}`;
417
+ if (!existingPaths.has(resultKey)) {
418
+ existingPaths.add(resultKey);
323
419
  supplementary.push(result);
324
420
  }
325
421
  }
@@ -355,14 +451,20 @@ ${responseFormat}`;
355
451
  .map(async (result) => {
356
452
  let content = result.excerpt;
357
453
  try {
358
- 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);
359
457
  const { content: fullContent } = await this.fileSystem.readFile(ctPath);
360
458
  content = fullContent;
361
459
  }
362
460
  catch {
363
461
  // Use excerpt if full read fails
364
462
  }
365
- 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 };
366
468
  }));
367
469
  if (!canRespondDirectly(fullResults))
368
470
  return undefined;
@@ -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
+ }
@@ -154,6 +154,17 @@ export class ConnectionCoordinator {
154
154
  return { error: 'Client not registered', success: false };
155
155
  }
156
156
  if (client.hasProject) {
157
+ // Idempotent: same path → no-op
158
+ if (client.projectPath === data.projectPath) {
159
+ return { success: true };
160
+ }
161
+ // Reassociation: path changed (e.g. after worktree add/remove)
162
+ const oldPath = this.clientManager.updateProjectPath(clientId, data.projectPath);
163
+ transportLog(`Client ${clientId} reassociated: ${oldPath} → ${data.projectPath}`);
164
+ if (oldPath) {
165
+ this.removeFromProjectRoom(clientId, oldPath);
166
+ }
167
+ this.addToProjectRoom(clientId, data.projectPath);
157
168
  return { success: true };
158
169
  }
159
170
  this.clientManager.associateProject(clientId, data.projectPath);
@@ -36,7 +36,7 @@ import { FileReviewBackupStore } from '../storage/file-review-backup-store.js';
36
36
  import { createTokenStore } from '../storage/token-store.js';
37
37
  import { HttpTeamService } from '../team/http-team-service.js';
38
38
  import { FsTemplateLoader } from '../template/fs-template-loader.js';
39
- import { AuthHandler, ConfigHandler, ConnectorsHandler, HubHandler, InitHandler, LocationsHandler, ModelHandler, ProviderHandler, PullHandler, PushHandler, ResetHandler, ReviewHandler, SpaceHandler, StatusHandler, VcHandler, } from '../transport/handlers/index.js';
39
+ import { AuthHandler, ConfigHandler, ConnectorsHandler, HubHandler, InitHandler, LocationsHandler, ModelHandler, ProviderHandler, PullHandler, PushHandler, ResetHandler, ReviewHandler, SourceHandler, SpaceHandler, StatusHandler, VcHandler, WorktreeHandler, } from '../transport/handlers/index.js';
40
40
  import { HttpUserService } from '../user/http-user-service.js';
41
41
  import { FileVcGitConfigStore } from '../vc/file-vc-git-config-store.js';
42
42
  /**
@@ -211,5 +211,8 @@ export async function setupFeatureHandlers({ authStateStore, broadcastToProject,
211
211
  vcGitConfigStore: new FileVcGitConfigStore(),
212
212
  webAppUrl: envConfig.webAppUrl,
213
213
  }).setup();
214
+ // Worktree & source handlers
215
+ new WorktreeHandler({ resolveProjectPath, transport }).setup();
216
+ new SourceHandler({ resolveProjectPath, transport }).setup();
214
217
  log('Feature handlers registered');
215
218
  }