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
@@ -4,11 +4,11 @@ import { SearchKnowledgeService } from './search-knowledge-service.js';
4
4
  const SearchKnowledgeInputSchema = z
5
5
  .object({
6
6
  excludeKinds: z
7
- .array(z.enum(['archive_stub', 'context', 'domain', 'subtopic', 'topic']))
7
+ .array(z.enum(['archive_stub', 'context', 'domain', 'subtopic', 'summary', 'topic']))
8
8
  .optional()
9
9
  .describe('Symbol kinds to exclude from results'),
10
10
  includeKinds: z
11
- .array(z.enum(['archive_stub', 'context', 'domain', 'subtopic', 'topic']))
11
+ .array(z.enum(['archive_stub', 'context', 'domain', 'subtopic', 'summary', 'topic']))
12
12
  .optional()
13
13
  .describe('Symbol kinds to include in results (filters out others)'),
14
14
  limit: z
@@ -1,3 +1,4 @@
1
+ import type { EnvironmentContext } from '../../../core/domain/environment/types.js';
1
2
  import type { Tool } from '../../../core/domain/tools/types.js';
2
3
  import type { IFileSystem } from '../../../core/interfaces/i-file-system.js';
3
4
  /**
@@ -9,4 +10,4 @@ import type { IFileSystem } from '../../../core/interfaces/i-file-system.js';
9
10
  * @param fileSystemService - File system service dependency
10
11
  * @returns Configured write file tool
11
12
  */
12
- export declare function createWriteFileTool(fileSystemService: IFileSystem): Tool;
13
+ export declare function createWriteFileTool(fileSystemService: IFileSystem, environmentContext?: EnvironmentContext): Tool;
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { sanitizeFolderName } from '../../../../server/utils/file-helpers.js';
3
3
  import { ToolName } from '../../../core/domain/tools/constants.js';
4
+ import { validateWriteTarget } from '../write-guard.js';
4
5
  /**
5
6
  * Input schema for write file tool.
6
7
  */
@@ -29,13 +30,26 @@ const WriteFileInputSchema = z
29
30
  * @param fileSystemService - File system service dependency
30
31
  * @returns Configured write file tool
31
32
  */
32
- export function createWriteFileTool(fileSystemService) {
33
+ export function createWriteFileTool(fileSystemService, environmentContext) {
33
34
  return {
34
35
  description: 'Write content to a file. Overwrites existing files. Can optionally create parent directories.',
35
36
  async execute(input, _context) {
36
37
  const { content, createDirs, encoding, filePath } = input;
38
+ const sanitizedPath = sanitizeFolderName(filePath);
39
+ // Write guard: block writes to shared source context trees
40
+ if (environmentContext?.workingDirectory) {
41
+ const writeError = validateWriteTarget(sanitizedPath, environmentContext.workingDirectory);
42
+ if (writeError) {
43
+ return {
44
+ bytesWritten: 0,
45
+ error: writeError,
46
+ path: sanitizedPath,
47
+ success: false,
48
+ };
49
+ }
50
+ }
37
51
  // Call file system service
38
- const result = await fileSystemService.writeFile(sanitizeFolderName(filePath), content, {
52
+ const result = await fileSystemService.writeFile(sanitizedPath, content, {
39
53
  createDirs,
40
54
  encoding: encoding,
41
55
  });
@@ -18,6 +18,7 @@ export class ToolProvider {
18
18
  * TypeScript ensures this object matches ToolServices keys at compile time.
19
19
  */
20
20
  static VALID_SERVICE_KEYS = new Set(Object.keys({
21
+ abstractQueue: 0,
21
22
  agentInstance: 0,
22
23
  contentGenerator: 0,
23
24
  environmentContext: 0,
@@ -9,6 +9,7 @@ import type { IProcessService } from '../../core/interfaces/i-process-service.js
9
9
  import type { ISandboxService } from '../../core/interfaces/i-sandbox-service.js';
10
10
  import type { ITodoStorage } from '../../core/interfaces/i-todo-storage.js';
11
11
  import type { ITokenizer } from '../../core/interfaces/i-tokenizer.js';
12
+ import type { AbstractGenerationQueue } from '../map/abstract-queue.js';
12
13
  import type { MemoryManager } from '../memory/memory-manager.js';
13
14
  import type { ToolProviderGetter } from './tool-provider-getter.js';
14
15
  import { ToolMarker } from './tool-markers.js';
@@ -17,6 +18,8 @@ import { ToolMarker } from './tool-markers.js';
17
18
  * Tools declare which services they need via requiredServices.
18
19
  */
19
20
  export interface ToolServices {
21
+ /** Abstract generation queue for background L0/L1 abstract file generation */
22
+ abstractQueue?: AbstractGenerationQueue;
20
23
  /** Agent instance for creating sub-sessions (used by agentic_map) */
21
24
  agentInstance?: ICipherAgent;
22
25
  /** Content generator for stateless LLM calls (used by llm_map) */
@@ -6,6 +6,7 @@ import { createCurateTool } from './implementations/curate-tool.js';
6
6
  import { createExpandKnowledgeTool } from './implementations/expand-knowledge-tool.js';
7
7
  import { createGlobFilesTool } from './implementations/glob-files-tool.js';
8
8
  import { createGrepContentTool } from './implementations/grep-content-tool.js';
9
+ import { createIngestResourceTool } from './implementations/ingest-resource-tool.js';
9
10
  import { createListDirectoryTool } from './implementations/list-directory-tool.js';
10
11
  import { createLlmMapTool } from './implementations/llm-map-tool.js';
11
12
  import { createReadFileTool } from './implementations/read-file-tool.js';
@@ -54,7 +55,7 @@ export const TOOL_REGISTRY = {
54
55
  },
55
56
  [ToolName.CODE_EXEC]: {
56
57
  descriptionFile: 'code_exec',
57
- factory({ environmentContext, fileSystemService, sandboxService }) {
58
+ factory({ abstractQueue, environmentContext, fileSystemService, sandboxService }) {
58
59
  const sandbox = getRequiredService(sandboxService, 'sandboxService');
59
60
  // Inject file system service into sandbox for Tools SDK
60
61
  if (fileSystemService && sandbox.setFileSystem) {
@@ -67,7 +68,7 @@ export const TOOL_REGISTRY = {
67
68
  }
68
69
  // Inject curate service into sandbox for Tools SDK
69
70
  if (sandbox.setCurateService) {
70
- const curateService = createCurateService(environmentContext?.workingDirectory);
71
+ const curateService = createCurateService(environmentContext?.workingDirectory, abstractQueue);
71
72
  sandbox.setCurateService(curateService);
72
73
  }
73
74
  // Inject environment context into sandbox for env.* access
@@ -81,10 +82,10 @@ export const TOOL_REGISTRY = {
81
82
  },
82
83
  [ToolName.CURATE]: {
83
84
  descriptionFile: 'curate',
84
- factory: ({ environmentContext }) => createCurateTool(environmentContext?.workingDirectory),
85
+ factory: ({ abstractQueue, environmentContext }) => createCurateTool(environmentContext?.workingDirectory, abstractQueue),
85
86
  markers: [ToolMarker.ContextBuilding, ToolMarker.Modification],
86
87
  outputGuidance: 'curate',
87
- requiredServices: [], // Uses DirectoryManager and MarkdownWriter for file operations
88
+ requiredServices: [],
88
89
  },
89
90
  [ToolName.EXPAND_KNOWLEDGE]: {
90
91
  descriptionFile: 'expand_knowledge',
@@ -104,6 +105,16 @@ export const TOOL_REGISTRY = {
104
105
  markers: [ToolMarker.Core, ToolMarker.Discovery],
105
106
  requiredServices: ['fileSystemService'],
106
107
  },
108
+ [ToolName.INGEST_RESOURCE]: {
109
+ factory: ({ abstractQueue, contentGenerator, environmentContext, fileSystemService }) => createIngestResourceTool({
110
+ abstractQueue,
111
+ baseDirectory: environmentContext?.workingDirectory,
112
+ contentGenerator,
113
+ fileSystem: fileSystemService,
114
+ }),
115
+ markers: [ToolMarker.ContextBuilding, ToolMarker.Modification],
116
+ requiredServices: ['contentGenerator', 'fileSystemService'],
117
+ },
107
118
  [ToolName.LIST_DIRECTORY]: {
108
119
  descriptionFile: 'list_directory',
109
120
  factory: (services) => createListDirectoryTool(getRequiredService(services.fileSystemService, 'fileSystemService')),
@@ -133,7 +144,7 @@ export const TOOL_REGISTRY = {
133
144
  },
134
145
  [ToolName.WRITE_FILE]: {
135
146
  descriptionFile: 'write_file',
136
- factory: (services) => createWriteFileTool(getRequiredService(services.fileSystemService, 'fileSystemService')),
147
+ factory: (services) => createWriteFileTool(getRequiredService(services.fileSystemService, 'fileSystemService'), services.environmentContext),
137
148
  markers: [ToolMarker.Modification],
138
149
  requiredServices: ['fileSystemService'],
139
150
  },
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Validates that a write target path is within the local project's context tree,
3
+ * not inside any shared knowledge source's context tree.
4
+ *
5
+ * Shared sources are read-only — agents must never write to them.
6
+ *
7
+ * @param targetPath - Absolute path being written to
8
+ * @param projectRoot - The local project root (owns .brv/)
9
+ * @returns null if write is allowed, or an error message string if blocked
10
+ */
11
+ export declare function validateWriteTarget(targetPath: string, projectRoot: string): null | string;
@@ -0,0 +1,48 @@
1
+ import { realpathSync } from 'node:fs';
2
+ import { basename, dirname, join, resolve } from 'node:path';
3
+ import { BRV_DIR, CONTEXT_TREE_DIR } from '../../../server/constants.js';
4
+ import { loadSources } from '../../../server/core/domain/source/source-schema.js';
5
+ import { isDescendantOf } from '../../../server/utils/path-utils.js';
6
+ const canonicalize = (path) => {
7
+ try {
8
+ return realpathSync(resolve(path));
9
+ }
10
+ catch {
11
+ // For non-existent files, try canonicalizing the parent
12
+ try {
13
+ const parent = dirname(resolve(path));
14
+ return join(realpathSync(parent), basename(path));
15
+ }
16
+ catch {
17
+ return resolve(path);
18
+ }
19
+ }
20
+ };
21
+ /**
22
+ * Validates that a write target path is within the local project's context tree,
23
+ * not inside any shared knowledge source's context tree.
24
+ *
25
+ * Shared sources are read-only — agents must never write to them.
26
+ *
27
+ * @param targetPath - Absolute path being written to
28
+ * @param projectRoot - The local project root (owns .brv/)
29
+ * @returns null if write is allowed, or an error message string if blocked
30
+ */
31
+ export function validateWriteTarget(targetPath, projectRoot) {
32
+ const localContextTree = resolve(projectRoot, BRV_DIR, CONTEXT_TREE_DIR);
33
+ const canonicalLocalContextTree = canonicalize(localContextTree);
34
+ const canonicalTarget = canonicalize(targetPath);
35
+ if (isDescendantOf(canonicalTarget, canonicalLocalContextTree)) {
36
+ return null;
37
+ }
38
+ // Load sources to get shared context tree roots
39
+ const loaded = loadSources(projectRoot);
40
+ for (const origin of loaded?.origins ?? []) {
41
+ const canonicalSharedRoot = canonicalize(origin.contextTreeRoot);
42
+ if (isDescendantOf(canonicalTarget, canonicalSharedRoot)) {
43
+ const alias = origin.alias ?? origin.originKey;
44
+ return `Cannot write to shared source "${alias}" — sources are read-only. Only the local context tree (${localContextTree}) is writable.`;
45
+ }
46
+ }
47
+ return `Cannot write outside the local context tree. Only the local context tree (${localContextTree}) is writable.`;
48
+ }
@@ -38,6 +38,15 @@ prompt: |
38
38
  - Maximum depth: 2 levels (domain → topic → subtopic)
39
39
  </DIRECTORIES>
40
40
 
41
+ <KNOWLEDGE_SOURCES>
42
+ This project may have knowledge sources pointing to other projects' context trees (read-only).
43
+ - Search results may include `origin: "shared"` results from shared sources
44
+ - Shared results include `originContextTreeRoot` — use `join(originContextTreeRoot, path)` for `readFile`
45
+ - Local results get a slight relevance boost over shared results
46
+ - You CANNOT curate, write, or modify shared source context trees — they are read-only
47
+ - Only the local `.brv/context-tree/` is writable
48
+ </KNOWLEDGE_SOURCES>
49
+
41
50
  <TOOL_QUICK_REFERENCE>
42
51
  **Queries:** `code_exec` with `tools.*` SDK (searchKnowledge, glob, grep, readFile, listDirectory)
43
52
  **Curation:** `code_exec` with `tools.curate()` using UPSERT (preferred), ADD, UPDATE, MERGE, DELETE
@@ -14,6 +14,10 @@ When searching the knowledge base, you may encounter results with `symbolKind: "
14
14
  - `fullContent`: Complete original content of the archived entry
15
15
  - `tokenCount`: Estimated token count of the full content
16
16
 
17
+ **Shared-source results:**
18
+ - For shared results (`origin: "shared"`), the `stubPath` is relative to the shared context tree
19
+ - Use `originContextTreeRoot` from the search result to resolve the full path
20
+
17
21
  **Example:**
18
22
  - Search returns: `{ path: "_archived/auth/jwt-tokens/refresh-flow.stub.md", symbolKind: "archive_stub" }`
19
23
  - Call: `expand_knowledge({ stubPath: "_archived/auth/jwt-tokens/refresh-flow.stub.md" })`
@@ -17,14 +17,24 @@ This tool enables semantic/fuzzy search across all curated knowledge without nee
17
17
  - `title`: Topic title
18
18
  - `excerpt`: Relevant content snippet
19
19
  - `score`: Relevance score (higher is better)
20
+ - `origin`: `"local"` or `"shared"` (shared = from a knowledge source pointing at another project)
21
+ - `originAlias`: Alias of the shared source (only for shared results)
22
+ - `originContextTreeRoot`: Absolute path to the shared context tree (only for shared results). Use `join(originContextTreeRoot, path)` to read full content.
20
23
  - `totalFound`: Total number of matches
21
24
  - `message`: Status message
22
25
 
26
+ **Knowledge sources:**
27
+ - Results may include entries from shared knowledge sources (read-only)
28
+ - Shared results have `origin: "shared"` and `originContextTreeRoot` for reading
29
+ - Local results are boosted slightly in relevance scoring
30
+ - You cannot curate or write to shared source context trees
31
+
23
32
  **Usage tips:**
24
33
  - Use descriptive queries: "authentication flow" works better than "auth"
25
34
  - Search is fuzzy: minor typos are tolerated
26
35
  - Results are ranked by relevance to your query
27
- - Use `read_file` on returned paths to view full content
36
+ - For local results, use `read_file` on returned paths to view full content
37
+ - For shared results, use `read_file` on `join(originContextTreeRoot, path)`
28
38
 
29
39
  **Examples:**
30
40
  - Query: "API authentication" - finds topics about auth design
@@ -85,7 +85,7 @@ Bad examples:
85
85
  const taskType = flags.folder?.length ? 'curate-folder' : 'curate';
86
86
  let providerContext;
87
87
  try {
88
- await withDaemonRetry(async (client, projectRoot) => {
88
+ await withDaemonRetry(async (client, projectRoot, worktreeRoot) => {
89
89
  const active = await client.requestWithAck(TransportStateEventNames.GET_PROVIDER_CONFIG);
90
90
  providerContext = { activeModel: active.activeModel, activeProvider: active.activeProvider };
91
91
  if (!active.activeProvider) {
@@ -94,7 +94,7 @@ Bad examples:
94
94
  if (active.providerKeyMissing) {
95
95
  throw new Error(providerMissingMessage(active.activeProvider, active.authMethod));
96
96
  }
97
- await this.submitTask({ client, content: resolvedContent, flags, format, projectRoot, taskType });
97
+ await this.submitTask({ client, content: resolvedContent, flags, format, projectRoot, taskType, worktreeRoot });
98
98
  }, {
99
99
  ...this.getDaemonClientOptions(),
100
100
  onRetry: format === 'text'
@@ -221,7 +221,7 @@ Bad examples:
221
221
  }
222
222
  }
223
223
  async submitTask(props) {
224
- const { client, content, flags, format, projectRoot, taskType } = props;
224
+ const { client, content, flags, format, projectRoot, taskType, worktreeRoot } = props;
225
225
  const hasFolders = Boolean(flags.folder?.length);
226
226
  const taskId = randomUUID();
227
227
  const taskPayload = {
@@ -232,6 +232,7 @@ Bad examples:
232
232
  ...(projectRoot ? { projectPath: projectRoot } : {}),
233
233
  taskId,
234
234
  type: taskType,
235
+ ...(worktreeRoot ? { worktreeRoot } : {}),
235
236
  };
236
237
  if (flags.detach) {
237
238
  const ack = await client.requestWithAck(TaskEvents.CREATE, taskPayload);
@@ -1,5 +1,5 @@
1
- import { findProjectRoot } from '@campfirein/brv-transport-client';
2
1
  import { Args, Command, Flags } from '@oclif/core';
2
+ import { resolveProject } from '../../../server/infra/project/resolve-project.js';
3
3
  import { FileCurateLogStore } from '../../../server/infra/storage/file-curate-log-store.js';
4
4
  import { CurateLogUseCase } from '../../../server/infra/usecase/curate-log-use-case.js';
5
5
  import { getProjectDataDir } from '../../../server/utils/path-utils.js';
@@ -77,7 +77,7 @@ export default class CurateView extends Command {
77
77
  const after = flags.since ? this.parseTime(flags.since, '--since') : undefined;
78
78
  const before = flags.before ? this.parseTime(flags.before, '--before') : undefined;
79
79
  const format = flags.format === 'json' ? 'json' : 'text';
80
- const projectRoot = (await findProjectRoot(process.cwd())) ?? process.cwd();
80
+ const projectRoot = resolveProject()?.projectRoot ?? process.cwd();
81
81
  const baseDir = getProjectDataDir(projectRoot);
82
82
  const store = new FileCurateLogStore({ baseDir });
83
83
  const useCase = new CurateLogUseCase({
@@ -1,5 +1,6 @@
1
1
  import { ensureDaemonRunning } from '@campfirein/brv-transport-client';
2
2
  import { Command } from '@oclif/core';
3
+ import { resolveProject } from '../../server/infra/project/resolve-project.js';
3
4
  import { resolveLocalServerMainPath } from '../../server/utils/server-main-resolver.js';
4
5
  import { startRepl } from '../../tui/repl-startup.js';
5
6
  /**
@@ -31,9 +32,21 @@ export default class Main extends Command {
31
32
  const detail = daemonResult.spawnError ? `: ${daemonResult.spawnError}` : '';
32
33
  this.error(`Failed to start daemon: timed out waiting for daemon to become ready${detail}\n\nRun 'brv restart' to force a clean restart.`);
33
34
  }
35
+ // Resolve project (workspace-link-aware) before starting TUI.
36
+ // Gracefully handle broken/malformed workspace links so TUI still starts
37
+ // (user can fix via /worktree remove from within the REPL).
38
+ let resolution = null;
39
+ try {
40
+ resolution = resolveProject();
41
+ }
42
+ catch {
43
+ // Broken workspace link — start TUI without resolved project (falls back to cwd)
44
+ }
34
45
  // Start the interactive REPL (TUI connects via connectToDaemon internally)
35
46
  await startRepl({
47
+ projectPath: resolution?.projectRoot,
36
48
  version: this.config.version,
49
+ worktreeRoot: resolution?.worktreeRoot,
37
50
  });
38
51
  this.exit(0);
39
52
  }
@@ -47,7 +47,7 @@ Bad:
47
47
  return;
48
48
  let providerContext;
49
49
  try {
50
- await withDaemonRetry(async (client, projectRoot) => {
50
+ await withDaemonRetry(async (client, projectRoot, worktreeRoot) => {
51
51
  const active = await client.requestWithAck(TransportStateEventNames.GET_PROVIDER_CONFIG);
52
52
  providerContext = { activeModel: active.activeModel, activeProvider: active.activeProvider };
53
53
  if (!active.activeProvider) {
@@ -56,7 +56,7 @@ Bad:
56
56
  if (active.providerKeyMissing) {
57
57
  throw new Error(providerMissingMessage(active.activeProvider, active.authMethod));
58
58
  }
59
- await this.submitTask({ client, format, projectRoot, query: args.query });
59
+ await this.submitTask({ client, format, projectRoot, query: args.query, worktreeRoot });
60
60
  }, {
61
61
  ...this.getDaemonClientOptions(),
62
62
  onRetry: format === 'text'
@@ -82,7 +82,7 @@ Bad:
82
82
  }
83
83
  }
84
84
  async submitTask(props) {
85
- const { client, format, projectRoot, query } = props;
85
+ const { client, format, projectRoot, query, worktreeRoot } = props;
86
86
  const taskId = randomUUID();
87
87
  const taskPayload = {
88
88
  clientCwd: process.cwd(),
@@ -90,6 +90,7 @@ Bad:
90
90
  ...(projectRoot ? { projectPath: projectRoot } : {}),
91
91
  taskId,
92
92
  type: 'query',
93
+ ...(worktreeRoot ? { worktreeRoot } : {}),
93
94
  };
94
95
  let finalResult;
95
96
  const completionPromise = waitForTaskCompletion({
@@ -0,0 +1,12 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class SourceAdd extends Command {
3
+ static args: {
4
+ path: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ alias: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ };
11
+ run(): Promise<void>;
12
+ }
@@ -0,0 +1,42 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { resolve } from 'node:path';
3
+ import { SourceEvents } from '../../../shared/transport/events/source-events.js';
4
+ import { formatConnectionError, withDaemonRetry } from '../../lib/daemon-client.js';
5
+ export default class SourceAdd extends Command {
6
+ static args = {
7
+ path: Args.string({
8
+ description: 'Path to the target project containing .brv/',
9
+ required: true,
10
+ }),
11
+ };
12
+ static description = "Add a read-only knowledge source from another project's context tree";
13
+ static examples = [
14
+ '<%= config.bin %> <%= command.id %> /path/to/shared-lib',
15
+ '<%= config.bin %> <%= command.id %> /path/to/shared-lib --alias shared',
16
+ ];
17
+ static flags = {
18
+ alias: Flags.string({
19
+ description: 'Custom alias for the source (defaults to directory name)',
20
+ required: false,
21
+ }),
22
+ };
23
+ async run() {
24
+ const { args, flags } = await this.parse(SourceAdd);
25
+ const targetPath = resolve(args.path);
26
+ try {
27
+ const result = await withDaemonRetry(async (client) => client.requestWithAck(SourceEvents.ADD, {
28
+ alias: flags.alias,
29
+ targetPath,
30
+ }), { projectPath: process.cwd() });
31
+ if (result.success) {
32
+ this.log(result.message);
33
+ }
34
+ else {
35
+ this.error(result.message, { exit: 1 });
36
+ }
37
+ }
38
+ catch (error) {
39
+ this.error(formatConnectionError(error), { exit: 1 });
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,6 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Source extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }
@@ -0,0 +1,8 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Source extends Command {
3
+ static description = 'Manage knowledge sources (read-only references to other projects)';
4
+ static examples = ['<%= config.bin %> <%= command.id %> --help'];
5
+ async run() {
6
+ await this.config.runCommand('help', ['source']);
7
+ }
8
+ }
@@ -0,0 +1,6 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class SourceList extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }
@@ -0,0 +1,32 @@
1
+ import { Command } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import { SourceEvents } from '../../../shared/transport/events/source-events.js';
4
+ import { formatConnectionError, withDaemonRetry } from '../../lib/daemon-client.js';
5
+ export default class SourceList extends Command {
6
+ static description = 'List all knowledge sources and their status';
7
+ static examples = ['<%= config.bin %> <%= command.id %>'];
8
+ async run() {
9
+ try {
10
+ const result = await withDaemonRetry(async (client) => client.requestWithAck(SourceEvents.LIST), { projectPath: process.cwd() });
11
+ if (result.error) {
12
+ this.error(result.error, { exit: 1 });
13
+ }
14
+ if (result.statuses.length === 0) {
15
+ this.log('No knowledge sources configured.');
16
+ return;
17
+ }
18
+ this.log('Knowledge Sources:');
19
+ for (const source of result.statuses) {
20
+ if (source.valid) {
21
+ this.log(` ${source.alias} → ${source.projectRoot} ${chalk.green('(valid)')}`);
22
+ }
23
+ else {
24
+ this.log(` ${source.alias} → ${source.projectRoot} ${chalk.red(`[BROKEN - run brv source remove ${source.alias}]`)}`);
25
+ }
26
+ }
27
+ }
28
+ catch (error) {
29
+ this.error(formatConnectionError(error), { exit: 1 });
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,9 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class SourceRemove extends Command {
3
+ static args: {
4
+ aliasOrPath: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ run(): Promise<void>;
9
+ }
@@ -0,0 +1,33 @@
1
+ import { Args, Command } from '@oclif/core';
2
+ import { SourceEvents } from '../../../shared/transport/events/source-events.js';
3
+ import { formatConnectionError, withDaemonRetry } from '../../lib/daemon-client.js';
4
+ export default class SourceRemove extends Command {
5
+ static args = {
6
+ aliasOrPath: Args.string({
7
+ description: 'Alias or path of the knowledge source to remove',
8
+ required: true,
9
+ }),
10
+ };
11
+ static description = 'Remove a knowledge source';
12
+ static examples = [
13
+ '<%= config.bin %> <%= command.id %> shared-lib',
14
+ '<%= config.bin %> <%= command.id %> /path/to/shared-lib',
15
+ ];
16
+ async run() {
17
+ const { args } = await this.parse(SourceRemove);
18
+ try {
19
+ const result = await withDaemonRetry(async (client) => client.requestWithAck(SourceEvents.REMOVE, {
20
+ aliasOrPath: args.aliasOrPath,
21
+ }), { projectPath: process.cwd() });
22
+ if (result.success) {
23
+ this.log(result.message);
24
+ }
25
+ else {
26
+ this.error(result.message, { exit: 1 });
27
+ }
28
+ }
29
+ catch (error) {
30
+ this.error(formatConnectionError(error), { exit: 1 });
31
+ }
32
+ }
33
+ }
@@ -6,8 +6,12 @@ export default class Status extends Command {
6
6
  static examples: string[];
7
7
  static flags: {
8
8
  format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
9
+ 'project-root': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
11
  };
10
- protected fetchStatus(options?: DaemonClientOptions): Promise<StatusDTO>;
12
+ protected fetchStatus(options?: DaemonClientOptions & {
13
+ projectRootFlag?: string;
14
+ }): Promise<StatusDTO>;
11
15
  run(): Promise<void>;
12
16
  private formatTextOutput;
13
17
  private logVcHint;