byterover-cli 3.1.0 → 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 (137) hide show
  1. package/.env.production +4 -0
  2. package/README.md +17 -0
  3. package/dist/agent/infra/agent/agent-schemas.d.ts +8 -0
  4. package/dist/agent/infra/agent/agent-schemas.js +1 -0
  5. package/dist/agent/infra/sandbox/curate-service.js +14 -0
  6. package/dist/agent/infra/sandbox/sandbox-service.js +1 -0
  7. package/dist/agent/infra/sandbox/tools-sdk.d.ts +10 -0
  8. package/dist/agent/infra/sandbox/tools-sdk.js +9 -1
  9. package/dist/agent/infra/tools/implementations/search-knowledge-service.js +214 -101
  10. package/dist/agent/infra/tools/implementations/write-file-tool.d.ts +2 -1
  11. package/dist/agent/infra/tools/implementations/write-file-tool.js +16 -2
  12. package/dist/agent/infra/tools/tool-registry.js +1 -1
  13. package/dist/agent/infra/tools/write-guard.d.ts +11 -0
  14. package/dist/agent/infra/tools/write-guard.js +48 -0
  15. package/dist/agent/resources/prompts/system-prompt.yml +9 -0
  16. package/dist/agent/resources/tools/expand_knowledge.txt +4 -0
  17. package/dist/agent/resources/tools/search_knowledge.txt +11 -1
  18. package/dist/oclif/commands/curate/index.js +4 -3
  19. package/dist/oclif/commands/curate/view.js +2 -2
  20. package/dist/oclif/commands/main.js +13 -0
  21. package/dist/oclif/commands/query.js +4 -3
  22. package/dist/oclif/commands/source/add.d.ts +12 -0
  23. package/dist/oclif/commands/source/add.js +42 -0
  24. package/dist/oclif/commands/source/index.d.ts +6 -0
  25. package/dist/oclif/commands/source/index.js +8 -0
  26. package/dist/oclif/commands/source/list.d.ts +6 -0
  27. package/dist/oclif/commands/source/list.js +32 -0
  28. package/dist/oclif/commands/source/remove.d.ts +9 -0
  29. package/dist/oclif/commands/source/remove.js +33 -0
  30. package/dist/oclif/commands/status.d.ts +5 -1
  31. package/dist/oclif/commands/status.js +41 -6
  32. package/dist/oclif/commands/worktree/add.d.ts +12 -0
  33. package/dist/oclif/commands/worktree/add.js +44 -0
  34. package/dist/oclif/commands/worktree/index.d.ts +6 -0
  35. package/dist/oclif/commands/worktree/index.js +8 -0
  36. package/dist/oclif/commands/worktree/list.d.ts +6 -0
  37. package/dist/oclif/commands/worktree/list.js +28 -0
  38. package/dist/oclif/commands/worktree/remove.d.ts +9 -0
  39. package/dist/oclif/commands/worktree/remove.js +35 -0
  40. package/dist/oclif/hooks/init/validate-brv-config.js +4 -0
  41. package/dist/oclif/lib/daemon-client.d.ts +4 -2
  42. package/dist/oclif/lib/daemon-client.js +19 -3
  43. package/dist/server/constants.d.ts +6 -0
  44. package/dist/server/constants.js +8 -0
  45. package/dist/server/core/domain/client/client-info.d.ts +7 -0
  46. package/dist/server/core/domain/client/client-info.js +11 -0
  47. package/dist/server/core/domain/project/worktrees-schema.d.ts +29 -0
  48. package/dist/server/core/domain/project/worktrees-schema.js +17 -0
  49. package/dist/server/core/domain/source/source-operations.d.ts +31 -0
  50. package/dist/server/core/domain/source/source-operations.js +201 -0
  51. package/dist/server/core/domain/source/source-schema.d.ts +94 -0
  52. package/dist/server/core/domain/source/source-schema.js +121 -0
  53. package/dist/server/core/domain/transport/schemas.d.ts +8 -0
  54. package/dist/server/core/domain/transport/schemas.js +4 -0
  55. package/dist/server/core/domain/transport/task-info.d.ts +2 -0
  56. package/dist/server/core/interfaces/client/i-client-manager.d.ts +13 -0
  57. package/dist/server/core/interfaces/executor/i-curate-executor.d.ts +4 -0
  58. package/dist/server/core/interfaces/executor/i-folder-pack-executor.d.ts +7 -3
  59. package/dist/server/core/interfaces/executor/i-query-executor.d.ts +2 -0
  60. package/dist/server/infra/client/client-manager.d.ts +1 -0
  61. package/dist/server/infra/client/client-manager.js +16 -0
  62. package/dist/server/infra/daemon/agent-process.js +15 -5
  63. package/dist/server/infra/executor/curate-executor.js +4 -2
  64. package/dist/server/infra/executor/direct-search-responder.js +5 -1
  65. package/dist/server/infra/executor/folder-pack-executor.js +23 -12
  66. package/dist/server/infra/executor/query-executor.d.ts +23 -0
  67. package/dist/server/infra/executor/query-executor.js +115 -21
  68. package/dist/server/infra/mcp/mcp-mode-detector.d.ts +7 -5
  69. package/dist/server/infra/mcp/mcp-mode-detector.js +11 -18
  70. package/dist/server/infra/mcp/mcp-server.d.ts +1 -0
  71. package/dist/server/infra/mcp/mcp-server.js +11 -6
  72. package/dist/server/infra/mcp/tools/brv-curate-tool.d.ts +2 -1
  73. package/dist/server/infra/mcp/tools/brv-curate-tool.js +9 -16
  74. package/dist/server/infra/mcp/tools/brv-query-tool.d.ts +2 -1
  75. package/dist/server/infra/mcp/tools/brv-query-tool.js +9 -16
  76. package/dist/server/infra/mcp/tools/mcp-project-context.d.ts +11 -0
  77. package/dist/server/infra/mcp/tools/mcp-project-context.js +54 -0
  78. package/dist/server/infra/process/connection-coordinator.js +11 -0
  79. package/dist/server/infra/process/feature-handlers.js +4 -1
  80. package/dist/server/infra/process/task-router.d.ts +1 -0
  81. package/dist/server/infra/process/task-router.js +60 -5
  82. package/dist/server/infra/project/resolve-project.d.ts +106 -0
  83. package/dist/server/infra/project/resolve-project.js +473 -0
  84. package/dist/server/infra/transport/handlers/index.d.ts +4 -0
  85. package/dist/server/infra/transport/handlers/index.js +2 -0
  86. package/dist/server/infra/transport/handlers/source-handler.d.ts +12 -0
  87. package/dist/server/infra/transport/handlers/source-handler.js +37 -0
  88. package/dist/server/infra/transport/handlers/status-handler.js +55 -13
  89. package/dist/server/infra/transport/handlers/worktree-handler.d.ts +12 -0
  90. package/dist/server/infra/transport/handlers/worktree-handler.js +67 -0
  91. package/dist/server/infra/transport/transport-connector.d.ts +10 -4
  92. package/dist/server/infra/transport/transport-connector.js +2 -2
  93. package/dist/server/utils/path-utils.d.ts +5 -0
  94. package/dist/server/utils/path-utils.js +11 -1
  95. package/dist/shared/transport/events/client-events.d.ts +3 -0
  96. package/dist/shared/transport/events/client-events.js +3 -0
  97. package/dist/shared/transport/events/index.d.ts +13 -0
  98. package/dist/shared/transport/events/index.js +9 -0
  99. package/dist/shared/transport/events/source-events.d.ts +30 -0
  100. package/dist/shared/transport/events/source-events.js +5 -0
  101. package/dist/shared/transport/events/status-events.d.ts +5 -0
  102. package/dist/shared/transport/events/task-events.d.ts +4 -1
  103. package/dist/shared/transport/events/worktree-events.d.ts +31 -0
  104. package/dist/shared/transport/events/worktree-events.js +5 -0
  105. package/dist/shared/transport/types/dto.d.ts +19 -0
  106. package/dist/tui/features/commands/definitions/index.js +6 -0
  107. package/dist/tui/features/commands/definitions/source-add.d.ts +2 -0
  108. package/dist/tui/features/commands/definitions/source-add.js +48 -0
  109. package/dist/tui/features/commands/definitions/source-list.d.ts +2 -0
  110. package/dist/tui/features/commands/definitions/source-list.js +47 -0
  111. package/dist/tui/features/commands/definitions/source-remove.d.ts +2 -0
  112. package/dist/tui/features/commands/definitions/source-remove.js +38 -0
  113. package/dist/tui/features/commands/definitions/source.d.ts +2 -0
  114. package/dist/tui/features/commands/definitions/source.js +8 -0
  115. package/dist/tui/features/commands/definitions/worktree-add.d.ts +2 -0
  116. package/dist/tui/features/commands/definitions/worktree-add.js +35 -0
  117. package/dist/tui/features/commands/definitions/worktree-list.d.ts +2 -0
  118. package/dist/tui/features/commands/definitions/worktree-list.js +36 -0
  119. package/dist/tui/features/commands/definitions/worktree-remove.d.ts +2 -0
  120. package/dist/tui/features/commands/definitions/worktree-remove.js +33 -0
  121. package/dist/tui/features/commands/definitions/worktree.d.ts +2 -0
  122. package/dist/tui/features/commands/definitions/worktree.js +8 -0
  123. package/dist/tui/features/curate/api/create-curate-task.js +3 -1
  124. package/dist/tui/features/query/api/create-query-task.js +3 -1
  125. package/dist/tui/features/source/api/source-api.d.ts +4 -0
  126. package/dist/tui/features/source/api/source-api.js +22 -0
  127. package/dist/tui/features/status/api/get-status.js +2 -1
  128. package/dist/tui/features/status/utils/format-status.js +23 -1
  129. package/dist/tui/features/transport/components/transport-initializer.js +36 -1
  130. package/dist/tui/features/worktree/api/worktree-api.d.ts +4 -0
  131. package/dist/tui/features/worktree/api/worktree-api.js +22 -0
  132. package/dist/tui/repl-startup.d.ts +2 -0
  133. package/dist/tui/repl-startup.js +5 -3
  134. package/dist/tui/stores/transport-store.d.ts +6 -0
  135. package/dist/tui/stores/transport-store.js +6 -0
  136. package/oclif.manifest.json +418 -158
  137. package/package.json +1 -1
@@ -0,0 +1,121 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { z } from 'zod';
5
+ import { BRV_DIR, CONTEXT_TREE_DIR, PROJECT_CONFIG_FILE, SOURCES_FILE } from '../../../constants.js';
6
+ // ============================================================================
7
+ // Schema
8
+ // ============================================================================
9
+ export const SourceSchema = z.object({
10
+ addedAt: z.string(),
11
+ alias: z.string().min(1),
12
+ projectRoot: z.string().min(1),
13
+ readOnly: z.literal(true),
14
+ });
15
+ export const SourcesFileSchema = z.object({
16
+ sources: z.array(SourceSchema),
17
+ version: z.literal(1),
18
+ });
19
+ /**
20
+ * Derives a stable, short origin key from a canonical path.
21
+ * Uses first 12 hex chars of SHA-256 to avoid alias-based collisions.
22
+ */
23
+ export function deriveOriginKey(canonicalPath) {
24
+ return createHash('sha256').update(canonicalPath).digest('hex').slice(0, 12);
25
+ }
26
+ /**
27
+ * Loads and validates `.brv/sources.json` from a project root.
28
+ *
29
+ * Returns null if the file does not exist.
30
+ * Broken sources (target `.brv/` missing) are included in `sources` but excluded
31
+ * from `origins` — callers decide how to surface them (status vs search).
32
+ */
33
+ export function loadSources(projectRoot) {
34
+ const filePath = join(projectRoot, BRV_DIR, SOURCES_FILE);
35
+ if (!existsSync(filePath)) {
36
+ return null;
37
+ }
38
+ const mtime = statSync(filePath).mtimeMs;
39
+ let raw;
40
+ try {
41
+ raw = JSON.parse(readFileSync(filePath, 'utf8'));
42
+ }
43
+ catch {
44
+ return { mtime, origins: [], sources: [] };
45
+ }
46
+ const result = SourcesFileSchema.safeParse(raw);
47
+ if (!result.success) {
48
+ return { mtime, origins: [], sources: [] };
49
+ }
50
+ const origins = [];
51
+ for (const source of result.data.sources) {
52
+ const targetConfigPath = join(source.projectRoot, BRV_DIR, PROJECT_CONFIG_FILE);
53
+ if (!existsSync(targetConfigPath)) {
54
+ // Broken source — skip from origins but keep in sources for status display
55
+ continue;
56
+ }
57
+ let canonicalRoot;
58
+ try {
59
+ canonicalRoot = realpathSync(source.projectRoot);
60
+ }
61
+ catch {
62
+ continue;
63
+ }
64
+ const contextTreeRoot = join(canonicalRoot, BRV_DIR, CONTEXT_TREE_DIR);
65
+ if (!existsSync(contextTreeRoot)) {
66
+ continue;
67
+ }
68
+ origins.push({
69
+ alias: source.alias,
70
+ contextTreeRoot,
71
+ origin: 'shared',
72
+ originKey: deriveOriginKey(canonicalRoot),
73
+ });
74
+ }
75
+ return { mtime, origins, sources: result.data.sources };
76
+ }
77
+ /**
78
+ * Validates each source and returns status for display.
79
+ *
80
+ * A source is valid only when both `.brv/config.json` AND `.brv/context-tree/`
81
+ * exist — matching what loadSources() requires before including a source in
82
+ * search origins. When valid, `contextTreeSize` counts `.md` files.
83
+ */
84
+ export function getSourceStatuses(sources) {
85
+ return sources.map((source) => {
86
+ const targetConfigPath = join(source.projectRoot, BRV_DIR, PROJECT_CONFIG_FILE);
87
+ const targetContextTree = join(source.projectRoot, BRV_DIR, CONTEXT_TREE_DIR);
88
+ const valid = existsSync(targetConfigPath) && existsSync(targetContextTree);
89
+ let contextTreeSize;
90
+ if (valid) {
91
+ contextTreeSize = countMarkdownFiles(targetContextTree);
92
+ }
93
+ return {
94
+ alias: source.alias,
95
+ contextTreeSize,
96
+ projectRoot: source.projectRoot,
97
+ valid,
98
+ };
99
+ });
100
+ }
101
+ /**
102
+ * Recursively counts .md files in a directory.
103
+ */
104
+ function countMarkdownFiles(dir) {
105
+ let count = 0;
106
+ try {
107
+ const entries = readdirSync(dir, { withFileTypes: true });
108
+ for (const entry of entries) {
109
+ if (entry.isDirectory()) {
110
+ count += countMarkdownFiles(join(dir, entry.name));
111
+ }
112
+ else if (entry.isFile() && entry.name.endsWith('.md')) {
113
+ count++;
114
+ }
115
+ }
116
+ }
117
+ catch {
118
+ // Directory unreadable — return 0
119
+ }
120
+ return count;
121
+ }
@@ -515,6 +515,8 @@ export declare const TaskExecuteSchema: z.ZodObject<{
515
515
  taskId: z.ZodString;
516
516
  /** Task type */
517
517
  type: z.ZodEnum<["curate", "curate-folder", "query"]>;
518
+ /** Workspace root for scoped query/curate */
519
+ worktreeRoot: z.ZodOptional<z.ZodString>;
518
520
  }, "strip", z.ZodTypeAny, {
519
521
  type: "curate" | "query" | "curate-folder";
520
522
  content: string;
@@ -524,6 +526,7 @@ export declare const TaskExecuteSchema: z.ZodObject<{
524
526
  clientCwd?: string | undefined;
525
527
  folderPath?: string | undefined;
526
528
  projectPath?: string | undefined;
529
+ worktreeRoot?: string | undefined;
527
530
  }, {
528
531
  type: "curate" | "query" | "curate-folder";
529
532
  content: string;
@@ -533,6 +536,7 @@ export declare const TaskExecuteSchema: z.ZodObject<{
533
536
  clientCwd?: string | undefined;
534
537
  folderPath?: string | undefined;
535
538
  projectPath?: string | undefined;
539
+ worktreeRoot?: string | undefined;
536
540
  }>;
537
541
  /**
538
542
  * task:cancel - Transport tells Agent to cancel a task
@@ -960,6 +964,8 @@ export declare const TaskCreateRequestSchema: z.ZodObject<{
960
964
  taskId: z.ZodString;
961
965
  /** Task type */
962
966
  type: z.ZodEnum<["curate", "curate-folder", "query"]>;
967
+ /** Workspace root for scoped query/curate (stable linked root or projectRoot if unlinked) */
968
+ worktreeRoot: z.ZodOptional<z.ZodString>;
963
969
  }, "strip", z.ZodTypeAny, {
964
970
  type: "curate" | "query" | "curate-folder";
965
971
  content: string;
@@ -968,6 +974,7 @@ export declare const TaskCreateRequestSchema: z.ZodObject<{
968
974
  clientCwd?: string | undefined;
969
975
  folderPath?: string | undefined;
970
976
  projectPath?: string | undefined;
977
+ worktreeRoot?: string | undefined;
971
978
  }, {
972
979
  type: "curate" | "query" | "curate-folder";
973
980
  content: string;
@@ -976,6 +983,7 @@ export declare const TaskCreateRequestSchema: z.ZodObject<{
976
983
  clientCwd?: string | undefined;
977
984
  folderPath?: string | undefined;
978
985
  projectPath?: string | undefined;
986
+ worktreeRoot?: string | undefined;
979
987
  }>;
980
988
  /**
981
989
  * Response after task creation
@@ -315,6 +315,8 @@ export const TaskExecuteSchema = z.object({
315
315
  taskId: z.string(),
316
316
  /** Task type */
317
317
  type: z.enum(['curate', 'curate-folder', 'query']),
318
+ /** Workspace root for scoped query/curate */
319
+ worktreeRoot: z.string().optional(),
318
320
  });
319
321
  /**
320
322
  * task:cancel - Transport tells Agent to cancel a task
@@ -492,6 +494,8 @@ export const TaskCreateRequestSchema = z.object({
492
494
  taskId: z.string().uuid('Invalid taskId format - must be UUID'),
493
495
  /** Task type */
494
496
  type: TaskTypeSchema,
497
+ /** Workspace root for scoped query/curate (stable linked root or projectRoot if unlinked) */
498
+ worktreeRoot: z.string().optional(),
495
499
  });
496
500
  /**
497
501
  * Response after task creation
@@ -18,4 +18,6 @@ export type TaskInfo = {
18
18
  projectPath?: string;
19
19
  taskId: string;
20
20
  type: TaskType;
21
+ /** Workspace root (linked subdir or projectRoot if unlinked) */
22
+ worktreeRoot?: string;
21
23
  };
@@ -114,4 +114,17 @@ export interface IClientManager {
114
114
  * @param clientId - The client's Socket.IO ID
115
115
  */
116
116
  unregister(clientId: string): void;
117
+ /**
118
+ * Update a client's project path, even if already associated.
119
+ * Used for reassociation after worktree add/remove operations.
120
+ * Moves the client from the old project index to the new one,
121
+ * and fires onProjectEmpty if the old project has no remaining external clients.
122
+ *
123
+ * No-op if client is unknown.
124
+ *
125
+ * @param clientId - The client's Socket.IO ID
126
+ * @param newProjectPath - The new project path to associate
127
+ * @returns The previous project path (undefined if client not found or not previously associated)
128
+ */
129
+ updateProjectPath(clientId: string, newProjectPath: string): string | undefined;
117
130
  }
@@ -10,8 +10,12 @@ export interface CurateExecuteOptions {
10
10
  content: string;
11
11
  /** Optional file paths for --files flag */
12
12
  files?: string[];
13
+ /** Canonical project root where .brv/ lives (for post-processing: snapshot, summary, manifest) */
14
+ projectRoot?: string;
13
15
  /** Task ID for event routing (required for concurrent task isolation) */
14
16
  taskId: string;
17
+ /** Workspace root — linked subdir or same as projectRoot for direct projects */
18
+ worktreeRoot?: string;
15
19
  }
16
20
  /**
17
21
  * ICurateExecutor - Executes curate tasks with an injected CipherAgent.
@@ -4,14 +4,18 @@ import type { ICipherAgent } from '../../../../agent/core/interfaces/i-cipher-ag
4
4
  * Agent uses its default session (Single-Session pattern).
5
5
  */
6
6
  export interface FolderPackExecuteOptions {
7
- /** Client's working directory for resolving relative paths */
7
+ /** Client's working directory for resolving relative paths (shell semantics) */
8
8
  clientCwd?: string;
9
9
  /** Optional context to guide the analysis */
10
10
  content?: string;
11
- /** Folder path to pack (relative to clientCwd or absolute) */
12
- folderPath: string;
11
+ /** Folder path to pack (relative to clientCwd or absolute). When absent, defaults to worktreeRoot. */
12
+ folderPath?: string;
13
+ /** Canonical project root where .brv/ lives (for temp file location) */
14
+ projectRoot?: string;
13
15
  /** Task ID for event routing (required for concurrent task isolation) */
14
16
  taskId: string;
17
+ /** Workspace root — linked subdir or same as projectRoot. Used as default folderPath when none supplied. */
18
+ worktreeRoot?: string;
15
19
  }
16
20
  /**
17
21
  * IFolderPackExecutor - Executes folder pack + curate tasks with an injected CipherAgent.
@@ -8,6 +8,8 @@ export interface QueryExecuteOptions {
8
8
  query: string;
9
9
  /** Task ID for event routing (required for concurrent task isolation) */
10
10
  taskId: string;
11
+ /** Stable workspace root for scoping search and cache isolation */
12
+ worktreeRoot?: string;
11
13
  }
12
14
  /**
13
15
  * IQueryExecutor - Executes query tasks with an injected CipherAgent.
@@ -41,6 +41,7 @@ export declare class ClientManager implements IClientManager {
41
41
  register(clientId: string, type: ClientType, projectPath?: string): void;
42
42
  setAgentName(clientId: string, agentName: string): void;
43
43
  unregister(clientId: string): void;
44
+ updateProjectPath(clientId: string, newProjectPath: string): string | undefined;
44
45
  private addToProjectIndex;
45
46
  /**
46
47
  * Check if a project has no remaining external clients.
@@ -111,6 +111,22 @@ export class ClientManager {
111
111
  // Notify idle timeout policy
112
112
  this.clientDisconnectedCallback?.();
113
113
  }
114
+ updateProjectPath(clientId, newProjectPath) {
115
+ const client = this.clients.get(clientId);
116
+ if (!client)
117
+ return undefined;
118
+ const oldPath = client.updateProjectPath(newProjectPath);
119
+ // Move between project indexes
120
+ if (oldPath) {
121
+ this.removeFromProjectIndex(clientId, oldPath);
122
+ }
123
+ this.addToProjectIndex(clientId, newProjectPath);
124
+ // Check if old project is now empty
125
+ if (oldPath && oldPath !== newProjectPath && client.isExternalClient) {
126
+ this.checkProjectEmpty(oldPath);
127
+ }
128
+ return oldPath;
129
+ }
114
130
  addToProjectIndex(clientId, projectPath) {
115
131
  let members = this.projectClients.get(projectPath);
116
132
  if (!members) {
@@ -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 {
@@ -58,7 +58,11 @@ export function formatDirectResponse(query, results) {
58
58
  return `### ${r.title}\n\n${truncatedContent}`;
59
59
  })
60
60
  .join('\n\n---\n\n');
61
- const sources = topResults.map((r) => `- \`.brv/context-tree/${r.path}\``).join('\n');
61
+ const sources = topResults.map((r) => {
62
+ // Paths starting with [ are already namespaced (linked results)
63
+ const displayPath = r.path.startsWith('[') ? r.path : `.brv/context-tree/${r.path}`;
64
+ return `- \`${displayPath}\``;
65
+ }).join('\n');
62
66
  return `**Summary**: ${summary}
63
67
 
64
68
  **Details**:
@@ -38,17 +38,28 @@ export class FolderPackExecutor {
38
38
  this.folderPackService = folderPackService;
39
39
  }
40
40
  async executeWithAgent(agent, options) {
41
- const { clientCwd, content, folderPath, taskId } = options;
41
+ const { clientCwd, content, folderPath, projectRoot, taskId, worktreeRoot } = options;
42
+ // Resolve folder path:
43
+ // - Absent folderPath → default to worktreeRoot (implicit workspace default)
44
+ // - Relative folderPath → resolve from clientCwd (shell semantics)
45
+ // - Absolute folderPath → use as-is
46
+ let absoluteFolderPath;
42
47
  if (!folderPath) {
43
- throw new Error('folderPath is required for curate-folder tasks');
48
+ absoluteFolderPath = worktreeRoot ?? clientCwd ?? process.cwd();
44
49
  }
45
- // Resolve folder path
46
- const basePath = clientCwd ?? process.cwd();
47
- const absoluteFolderPath = path.isAbsolute(folderPath) ? folderPath : path.resolve(basePath, folderPath);
48
- const snapshotService = new FileContextTreeSnapshotService({ baseDirectory: basePath });
50
+ else if (path.isAbsolute(folderPath)) {
51
+ absoluteFolderPath = folderPath;
52
+ }
53
+ else {
54
+ const shellCwd = clientCwd ?? process.cwd();
55
+ absoluteFolderPath = path.resolve(shellCwd, folderPath);
56
+ }
57
+ // Temp file location: use projectRoot where .brv/ lives (accessible to sandbox)
58
+ const tempFileDir = projectRoot ?? clientCwd ?? process.cwd();
59
+ const snapshotService = new FileContextTreeSnapshotService({ baseDirectory: tempFileDir });
49
60
  let preState;
50
61
  try {
51
- preState = await snapshotService.getCurrentState(basePath);
62
+ preState = await snapshotService.getCurrentState(tempFileDir);
52
63
  }
53
64
  catch {
54
65
  // Fail-open: if snapshot fails, skip summary propagation
@@ -62,17 +73,17 @@ export class FolderPackExecutor {
62
73
  // Use iterative extraction strategy (inspired by rlm)
63
74
  // Stores packed folder in sandbox environment and lets agent iteratively query/extract
64
75
  // This avoids token limits entirely - works for folders of any size
65
- const response = await this.executeIterative(agent, packResult, content, absoluteFolderPath, taskId, basePath);
76
+ const response = await this.executeIterative(agent, packResult, content, absoluteFolderPath, taskId, tempFileDir);
66
77
  if (preState) {
67
78
  try {
68
- const postState = await snapshotService.getCurrentState(basePath);
79
+ const postState = await snapshotService.getCurrentState(tempFileDir);
69
80
  const changedPaths = diffStates(preState, postState);
70
81
  if (changedPaths.length > 0) {
71
82
  const summaryService = new FileContextTreeSummaryService();
72
- const results = await summaryService.propagateStaleness(changedPaths, agent, basePath);
83
+ const results = await summaryService.propagateStaleness(changedPaths, agent, tempFileDir);
73
84
  if (results.some((result) => result.actionTaken)) {
74
- const manifestService = new FileContextTreeManifestService({ baseDirectory: basePath });
75
- await manifestService.buildManifest(basePath);
85
+ const manifestService = new FileContextTreeManifestService({ baseDirectory: tempFileDir });
86
+ await manifestService.buildManifest(tempFileDir);
76
87
  }
77
88
  }
78
89
  }
@@ -64,8 +64,31 @@ export declare class QueryExecutor implements IQueryExecutor {
64
64
  * Compute a context tree fingerprint cheaply using file mtimes.
65
65
  * Used for cache invalidation — if any file in the context tree changes,
66
66
  * the fingerprint changes and cached results are invalidated.
67
+ *
68
+ * Includes worktreeRoot in the hash so different workspaces produce
69
+ * different fingerprints, preventing cross-workspace cache bleed.
67
70
  */
68
71
  private computeContextTreeFingerprint;
72
+ /**
73
+ * Lightweight hash of currently valid shared source keys.
74
+ * Used by the fingerprint cache fast path to detect when a source target
75
+ * becomes broken (directory deleted) within the TTL window.
76
+ * Cost: one readFileSync + existsSync per source — sub-millisecond for typical setups.
77
+ */
78
+ private computeSourceValidityHash;
79
+ /**
80
+ * Derive a workspace scope for search from the worktreeRoot.
81
+ * Returns the relative path from projectRoot to worktreeRoot,
82
+ * or undefined if they are the same (no scoping needed).
83
+ *
84
+ * KNOWN LIMITATION: Workspace scoping only works if the curated context
85
+ * tree has a subtree matching the workspace relative path (e.g., 'packages/api').
86
+ * Since the context tree is organized semantically by the LLM (topic-based),
87
+ * not by directory structure, scope filtering typically has 0 matches and
88
+ * falls through to unscoped search. A proper fix requires tagging curated
89
+ * files with source workspace metadata during curation.
90
+ */
91
+ private deriveWorkspaceScope;
69
92
  /**
70
93
  * Extract key entities from a query for supplementary searches.
71
94
  * Simple heuristic: split query, filter stopwords, keep significant terms.