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
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import { readdir, readFile, stat, writeFile } from 'node:fs/promises';
17
17
  import { join, relative } from 'node:path';
18
- import { ARCHIVE_DIR, BRV_DIR, CONTEXT_FILE_EXTENSION, CONTEXT_TREE_DIR, MANIFEST_FILE, STUB_EXTENSION, SUMMARY_INDEX_FILE, } from '../../constants.js';
18
+ import { ABSTRACT_EXTENSION, ARCHIVE_DIR, BRV_DIR, CONTEXT_FILE_EXTENSION, CONTEXT_TREE_DIR, MANIFEST_FILE, STUB_EXTENSION, SUMMARY_INDEX_FILE, } from '../../constants.js';
19
19
  import { parseFrontmatterScoring } from '../../core/domain/knowledge/markdown-writer.js';
20
20
  import { DEFAULT_LANE_BUDGETS } from '../../core/domain/knowledge/summary-types.js';
21
21
  import { estimateTokens } from '../executor/pre-compaction/compaction-escalation.js';
@@ -98,8 +98,23 @@ export class FileContextTreeManifestService {
98
98
  /* eslint-disable no-await-in-loop */
99
99
  for (const entry of ordered) {
100
100
  try {
101
- const fullPath = join(contextTreeDir, entry.path);
102
- const content = await readFile(fullPath, 'utf8');
101
+ let content;
102
+ // For context entries, prefer .abstract.md sibling if it exists on disk
103
+ // (dynamic read avoids stale-manifest issues since .abstract.md is a derived artifact)
104
+ if (entry.type === 'context') {
105
+ const abstractRelPath = entry.path.replace(/\.md$/, ABSTRACT_EXTENSION);
106
+ const abstractFullPath = join(contextTreeDir, abstractRelPath);
107
+ try {
108
+ content = await readFile(abstractFullPath, 'utf8');
109
+ }
110
+ catch {
111
+ // Abstract not ready yet — fall back to full content
112
+ content = await readFile(join(contextTreeDir, entry.path), 'utf8');
113
+ }
114
+ }
115
+ else {
116
+ content = await readFile(join(contextTreeDir, entry.path), 'utf8');
117
+ }
103
118
  resolved.push({
104
119
  content,
105
120
  path: entry.path,
@@ -188,6 +203,12 @@ export class FileContextTreeManifestService {
188
203
  catch {
189
204
  return;
190
205
  }
206
+ // Build a set of abstract sibling paths present in this directory so we can
207
+ // check existence without throwing ENOENT for every context file that has
208
+ // no abstract yet (the common case early in a project's lifetime).
209
+ const abstractsInDir = new Set(entries
210
+ .filter((e) => e.isFile() && e.name.endsWith(ABSTRACT_EXTENSION))
211
+ .map((e) => join(currentDir, e.name)));
191
212
  /* eslint-disable no-await-in-loop */
192
213
  for (const entry of entries) {
193
214
  const entryName = entry.name;
@@ -221,10 +242,24 @@ export class FileContextTreeManifestService {
221
242
  try {
222
243
  const content = await readFile(fullPath, 'utf8');
223
244
  const scoring = parseFrontmatterScoring(content);
245
+ // Use abstract sibling for token budgeting only if it is known to exist
246
+ // (checked via abstractsInDir set, avoiding ENOENT as control flow).
247
+ const abstractRelPath = relativePath.replace(/\.md$/, ABSTRACT_EXTENSION);
248
+ const abstractFullPath = join(contextTreeDir, abstractRelPath);
249
+ let abstractTokens;
250
+ if (abstractsInDir.has(abstractFullPath)) {
251
+ try {
252
+ const abstractContent = await readFile(abstractFullPath, 'utf8');
253
+ abstractTokens = estimateTokens(abstractContent);
254
+ }
255
+ catch { /* unreadable — treat as absent */ }
256
+ }
224
257
  contexts.push({
258
+ abstractPath: abstractTokens === undefined ? undefined : abstractRelPath,
259
+ abstractTokens,
225
260
  importance: scoring?.importance ?? 50,
226
261
  path: relativePath,
227
- tokens: estimateTokens(content),
262
+ tokens: abstractTokens ?? estimateTokens(content),
228
263
  type: 'context',
229
264
  });
230
265
  }
@@ -238,7 +273,8 @@ export class FileContextTreeManifestService {
238
273
  }
239
274
  /**
240
275
  * Recursively collect stat data for all source files (for fingerprint).
241
- * Excludes derived artifacts.
276
+ * Excludes derived artifacts except .abstract.md siblings, which are included
277
+ * so abstract generation invalidates the manifest without a second tree walk.
242
278
  */
243
279
  async scanSourceStats(currentDir, contextTreeDir, entries) {
244
280
  let dirEntries;
@@ -257,8 +293,8 @@ export class FileContextTreeManifestService {
257
293
  }
258
294
  else if (entry.isFile() && entryName.endsWith(CONTEXT_FILE_EXTENSION)) {
259
295
  const relativePath = toUnixPath(relative(contextTreeDir, fullPath));
260
- // Include all non-derived files (contexts + stubs) in fingerprint
261
- if (isDerivedArtifact(relativePath))
296
+ const isAbstractSibling = entryName.endsWith(ABSTRACT_EXTENSION);
297
+ if (!isAbstractSibling && isDerivedArtifact(relativePath))
262
298
  continue;
263
299
  try {
264
300
  const fileStat = await stat(fullPath);
@@ -74,7 +74,18 @@ export class FileContextTreeSummaryService {
74
74
  // Step 5: Three-tier escalation via CipherAgent
75
75
  const taskId = `summary_${directoryPath.replaceAll('/', '_') || 'root'}`;
76
76
  const childEntries = children.map((c) => ({ content: c.content, name: c.name }));
77
- const summaryText = await this.generateWithEscalation(agent, taskId, childEntries, level, totalInputTokens);
77
+ let summaryText;
78
+ try {
79
+ summaryText = await this.generateWithEscalation(agent, taskId, childEntries, level, totalInputTokens);
80
+ }
81
+ catch {
82
+ const combinedInput = childEntries.map((entry) => `## ${entry.name}\n${entry.content}`).join('\n\n');
83
+ summaryText = buildDeterministicFallbackCompaction({
84
+ inputTokens: totalInputTokens,
85
+ sourceText: combinedInput,
86
+ suffixLabel: 'summary compaction',
87
+ });
88
+ }
78
89
  // Step 6: Write _index.md
79
90
  const summaryTokens = estimateTokens(summaryText);
80
91
  const frontmatter = {
@@ -289,7 +300,14 @@ export class FileContextTreeSummaryService {
289
300
  });
290
301
  }
291
302
  finally {
292
- await agent.deleteTaskSession(sessionId);
303
+ try {
304
+ // Cleanup is best-effort. A generated summary should still be written even
305
+ // if the backing task session cannot be torn down cleanly.
306
+ await agent.deleteTaskSession(sessionId);
307
+ }
308
+ catch {
309
+ // Ignore cleanup failures
310
+ }
293
311
  }
294
312
  }
295
313
  /**
@@ -31,6 +31,7 @@ import { AuthEvents } from '../../../shared/transport/events/auth-events.js';
31
31
  import { getCurrentConfig } from '../../config/environment.js';
32
32
  import { DEFAULT_LLM_MODEL, PROJECT } from '../../constants.js';
33
33
  import { serializeTaskError, TaskError, TaskErrorCode } from '../../core/domain/errors/task-error.js';
34
+ import { loadSources } from '../../core/domain/source/source-schema.js';
34
35
  import { TransportAgentEventNames, TransportDaemonEventNames, TransportStateEventNames, TransportTaskEventNames, } from '../../core/domain/transport/schemas.js';
35
36
  import { CurateExecutor } from '../executor/curate-executor.js';
36
37
  import { FolderPackExecutor } from '../executor/folder-pack-executor.js';
@@ -189,10 +190,14 @@ async function start() {
189
190
  cachedProviderHeaders = providerResult.providerHeaders ? JSON.stringify(providerResult.providerHeaders) : undefined;
190
191
  agentLog(`Provider: ${activeProvider}, Model: ${activeModel ?? 'default'}`);
191
192
  // 5. Create CipherAgent with lazy providers + transport client
193
+ // Load knowledge sources early so shared context tree roots can be shared with both
194
+ // the agent's FileSystemService (via config) and the executor's FileSystemService
195
+ const sourcesData = loadSources(projectPath);
196
+ const sharedAllowedPaths = (sourcesData?.origins ?? []).map((o) => o.contextTreeRoot);
192
197
  const envConfig = getCurrentConfig();
193
198
  const agentConfig = {
194
199
  apiBaseUrl: envConfig.llmApiBaseUrl,
195
- fileSystem: { workingDirectory: projectPath },
200
+ fileSystem: { allowedPaths: ['.', ...sharedAllowedPaths], workingDirectory: projectPath },
196
201
  llm: {
197
202
  maxIterations: 10,
198
203
  maxTokens: 4096,
@@ -279,7 +284,10 @@ async function start() {
279
284
  }
280
285
  });
281
286
  // 6. Create FileSystemService + SearchKnowledgeService for smart query routing
282
- const fileSystemService = new FileSystemService({ workingDirectory: projectPath });
287
+ const fileSystemService = new FileSystemService({
288
+ allowedPaths: ['.', ...sharedAllowedPaths],
289
+ workingDirectory: projectPath,
290
+ });
283
291
  await fileSystemService.initialize();
284
292
  const searchService = createSearchKnowledgeService(fileSystemService, { baseDirectory: projectPath });
285
293
  // 7. Create executors and listen for task:execute from pool
@@ -305,7 +313,7 @@ async function start() {
305
313
  agentLog('Ready — listening for tasks');
306
314
  }
307
315
  async function executeTask(task, curateExecutor, folderPackExecutor, queryExecutor) {
308
- const { clientCwd, clientId, content, files, folderPath, taskId, type } = task;
316
+ const { clientCwd, clientId, content, files, folderPath, taskId, type, worktreeRoot } = task;
309
317
  if (!transport || !agent)
310
318
  return;
311
319
  const freshProviderConfig = await transport.requestWithAck(TransportStateEventNames.GET_PROVIDER_CONFIG);
@@ -372,7 +380,7 @@ async function executeTask(task, curateExecutor, folderPackExecutor, queryExecut
372
380
  let result;
373
381
  switch (type) {
374
382
  case 'curate': {
375
- result = await curateExecutor.executeWithAgent(agent, { clientCwd, content, files, taskId });
383
+ result = await curateExecutor.executeWithAgent(agent, { clientCwd, content, files, projectRoot: projectPath, taskId, worktreeRoot });
376
384
  break;
377
385
  }
378
386
  case 'curate-folder': {
@@ -380,12 +388,14 @@ async function executeTask(task, curateExecutor, folderPackExecutor, queryExecut
380
388
  clientCwd,
381
389
  content,
382
390
  folderPath: folderPath,
391
+ projectRoot: projectPath,
383
392
  taskId,
393
+ worktreeRoot,
384
394
  });
385
395
  break;
386
396
  }
387
397
  case 'query': {
388
- result = await queryExecutor.executeWithAgent(agent, { query: content, taskId });
398
+ result = await queryExecutor.executeWithAgent(agent, { query: content, taskId, worktreeRoot });
389
399
  break;
390
400
  }
391
401
  }
@@ -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 {
@@ -54,7 +56,7 @@ export class CurateExecutor {
54
56
  catch {
55
57
  // Fail-open: if snapshot fails, skip summary propagation
56
58
  }
57
- const taskSessionId = await agent.createTaskSession(taskId, 'curate', { mapRootEligible: true });
59
+ const taskSessionId = await agent.createTaskSession(taskId, 'curate', { mapRootEligible: true, userFacing: true });
58
60
  try {
59
61
  // Task-scoped variable names for RLM pattern.
60
62
  // Replace hyphens with underscores: UUIDs have hyphens which are invalid in JS identifiers,
@@ -124,6 +126,7 @@ export class CurateExecutor {
124
126
  // Fail-open: summary/manifest errors never block curation
125
127
  }
126
128
  }
129
+ await agent.drainBackgroundWork?.();
127
130
  return response;
128
131
  }
129
132
  finally {
@@ -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**:
@@ -1,6 +1,10 @@
1
1
  import { appendFileSync } from 'node:fs';
2
2
  import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
+ import { FileContextTreeManifestService } from '../context-tree/file-context-tree-manifest-service.js';
5
+ import { FileContextTreeSnapshotService } from '../context-tree/file-context-tree-snapshot-service.js';
6
+ import { FileContextTreeSummaryService } from '../context-tree/file-context-tree-summary-service.js';
7
+ import { diffStates } from '../context-tree/snapshot-diff.js';
4
8
  const LOG_PATH = process.env.BRV_SESSION_LOG;
5
9
  function folderPackLog(message) {
6
10
  if (!LOG_PATH)
@@ -34,13 +38,32 @@ export class FolderPackExecutor {
34
38
  this.folderPackService = folderPackService;
35
39
  }
36
40
  async executeWithAgent(agent, options) {
37
- 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;
38
47
  if (!folderPath) {
39
- throw new Error('folderPath is required for curate-folder tasks');
48
+ absoluteFolderPath = worktreeRoot ?? clientCwd ?? process.cwd();
49
+ }
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 });
60
+ let preState;
61
+ try {
62
+ preState = await snapshotService.getCurrentState(tempFileDir);
63
+ }
64
+ catch {
65
+ // Fail-open: if snapshot fails, skip summary propagation
40
66
  }
41
- // Resolve folder path
42
- const basePath = clientCwd ?? process.cwd();
43
- const absoluteFolderPath = path.isAbsolute(folderPath) ? folderPath : path.resolve(basePath, folderPath);
44
67
  // Pack the folder
45
68
  const packResult = await this.folderPackService.pack(absoluteFolderPath, {
46
69
  extractDocuments: true,
@@ -50,7 +73,26 @@ export class FolderPackExecutor {
50
73
  // Use iterative extraction strategy (inspired by rlm)
51
74
  // Stores packed folder in sandbox environment and lets agent iteratively query/extract
52
75
  // This avoids token limits entirely - works for folders of any size
53
- return this.executeIterative(agent, packResult, content, absoluteFolderPath, taskId, basePath);
76
+ const response = await this.executeIterative(agent, packResult, content, absoluteFolderPath, taskId, tempFileDir);
77
+ if (preState) {
78
+ try {
79
+ const postState = await snapshotService.getCurrentState(tempFileDir);
80
+ const changedPaths = diffStates(preState, postState);
81
+ if (changedPaths.length > 0) {
82
+ const summaryService = new FileContextTreeSummaryService();
83
+ const results = await summaryService.propagateStaleness(changedPaths, agent, tempFileDir);
84
+ if (results.some((result) => result.actionTaken)) {
85
+ const manifestService = new FileContextTreeManifestService({ baseDirectory: tempFileDir });
86
+ await manifestService.buildManifest(tempFileDir);
87
+ }
88
+ }
89
+ }
90
+ catch {
91
+ // Fail-open: summary/manifest errors never block curation
92
+ }
93
+ }
94
+ await agent.drainBackgroundWork?.();
95
+ return response;
54
96
  }
55
97
  /**
56
98
  * Build iterative extraction prompt with file-based access.
@@ -133,6 +175,15 @@ Use **code_exec with tools.readFile/tools.grep** to extract knowledge:
133
175
 
134
176
  **Important**: All tools.* methods are async - always use \`await\`!
135
177
 
178
+ ## Curate Shape Constraints
179
+
180
+ - Prefer one concrete knowledge entry per relevant source file when curating a small leaf folder.
181
+ - For folders with 3 or fewer relevant source files, keep the number of curated leaf entries at or below the number of curated source files by default.
182
+ - Do **NOT** create an extra module/folder "overview" leaf at the bare topic path just because the folder has multiple files.
183
+ - Treat bare topic paths as scopes for \`topicContext\`, not as default destinations for standalone knowledge files.
184
+ - If you need topic-level framing, provide \`topicContext\` on the operation that creates the topic. The system will create/update \`context.md\` and higher-level summaries separately.
185
+ - Only add a standalone overview leaf when the user explicitly asks for it or when there is a distinct cross-file concept that cannot be represented by the per-file entries.
186
+
136
187
  ## Common Mistakes to Avoid
137
188
 
138
189
  **❌ WRONG - Using require() inside code_exec:**
@@ -773,7 +824,7 @@ await tools.curate([{
773
824
  throw new Error(`Failed to write temp file: ${error instanceof Error ? error.message : String(error)}`);
774
825
  }
775
826
  // Create per-task session for parallel isolation (own sandbox + history + LLM service)
776
- const taskSessionId = await agent.createTaskSession(taskId, 'curate', { mapRootEligible: true });
827
+ const taskSessionId = await agent.createTaskSession(taskId, 'curate', { mapRootEligible: true, userFacing: true });
777
828
  // Step 3: Store full instructions as sandbox variable (lazy prompt loading).
778
829
  // This saves ~12-15K tokens by keeping the massive instruction set out of the prompt.
779
830
  // The LLM reads instructions on-demand via code_exec.
@@ -783,15 +834,45 @@ await tools.curate([{
783
834
  const taskIdSafe = taskId.replaceAll('-', '_');
784
835
  const instructionsVar = `__curate_instructions_${taskIdSafe}`;
785
836
  agent.setSandboxVariableOnSession(taskSessionId, instructionsVar, fullInstructions);
837
+ const smallFolderFilesVar = `__curate_files_${taskIdSafe}`;
838
+ const shouldExposePackedFiles = packResult.files.length > 0 && packResult.files.length <= 10 && packResult.totalCharacters <= 80_000;
839
+ if (shouldExposePackedFiles) {
840
+ agent.setSandboxVariableOnSession(taskSessionId, smallFolderFilesVar, packResult.files.map((file) => ({
841
+ content: file.content,
842
+ fileType: file.fileType,
843
+ lineCount: file.lineCount,
844
+ path: file.path,
845
+ size: file.size,
846
+ truncated: file.truncated,
847
+ })));
848
+ }
786
849
  // Compact prompt with variable reference and essential metadata
787
850
  const contextSection = userContext?.trim() ? `\nUser context: ${userContext}\n` : '';
851
+ const sourceFilePaths = packResult.files.map((file) => file.path);
852
+ const sourceFilesSection = sourceFilePaths.length > 0
853
+ ? `Relevant source files: ${sourceFilePaths.join(', ')} (these paths are relative to the packed folder root; do not prefix them with parent directories like src/auth/)`
854
+ : undefined;
855
+ const smallFolderLeafQuota = sourceFilePaths.length > 0 && sourceFilePaths.length <= 3
856
+ ? `Leaf quota: create no more than ${sourceFilePaths.length} curated leaf knowledge files for this folder unless the user explicitly asks for more.`
857
+ : undefined;
858
+ const smallFolderQuotaWarning = sourceFilePaths.length > 0 && sourceFilePaths.length <= 3
859
+ ? `A topic-level overview leaf counts toward that quota and is usually incorrect here; keep folder-level framing in topicContext instead.`
860
+ : undefined;
788
861
  const compactPrompt = [
789
862
  `# Folder Curation Task`,
790
863
  ``,
791
864
  `Folder: ${folderPath} (${packResult.fileCount} files, ${packResult.totalLines} lines)`,
792
865
  `Data file: \`${tmpFilePath}\` (repomix-style XML format)`,
793
866
  `Full instructions: variable \`${instructionsVar}\``,
867
+ shouldExposePackedFiles
868
+ ? `Relevant files variable: \`${smallFolderFilesVar}\` (array of packed files; for this small folder, prefer using it directly instead of parsing XML with brittle regexes).`
869
+ : undefined,
794
870
  contextSection,
871
+ sourceFilesSection,
872
+ `Small-folder rule: for folders with 3 or fewer relevant source files, create at most one leaf knowledge entry per file by default.`,
873
+ smallFolderLeafQuota,
874
+ smallFolderQuotaWarning,
875
+ `Do not create an extra overview leaf at the bare topic path; use topicContext for topic-level framing instead.`,
795
876
  `**Start by reading instructions**: Use code_exec to read \`${instructionsVar}.slice(0, 5000)\` for the strategy section, then \`${instructionsVar}.slice(5000, 10000)\` for content rules.`,
796
877
  `Use \`tools.readFile()\` and \`tools.grep()\` inside code_exec to process the XML data file.`,
797
878
  `Use \`tools.curate()\` to create knowledge topics. Use \`setFinalResult()\` when done.`,
@@ -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.