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
@@ -0,0 +1,201 @@
1
+ import { existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
2
+ import { basename, join } from 'node:path';
3
+ import { BRV_DIR, PROJECT_CONFIG_FILE, SOURCES_FILE } from '../../../constants.js';
4
+ import { getSourceStatuses, loadSources, SourcesFileSchema, } from './source-schema.js';
5
+ // ============================================================================
6
+ // Add source
7
+ // ============================================================================
8
+ /**
9
+ * Adds a read-only knowledge source from another project's context tree.
10
+ *
11
+ * Validates: target is a brv project, not self, not duplicate, not circular.
12
+ * Writes to `.brv/sources.json`.
13
+ */
14
+ export function addSource(projectRoot, targetPath, alias) {
15
+ // 1. Local project must have .brv/
16
+ const localConfigPath = join(projectRoot, BRV_DIR, PROJECT_CONFIG_FILE);
17
+ if (!existsSync(localConfigPath)) {
18
+ return { message: `Current project has no .brv/ — run 'brv' first to initialize.`, success: false };
19
+ }
20
+ // 2. Resolve target to canonical path
21
+ let targetRoot;
22
+ try {
23
+ targetRoot = realpathSync(targetPath);
24
+ }
25
+ catch {
26
+ return { message: `Target path does not exist: ${targetPath}`, success: false };
27
+ }
28
+ // 3. Target must be a brv project
29
+ const targetConfigPath = join(targetRoot, BRV_DIR, PROJECT_CONFIG_FILE);
30
+ if (!existsSync(targetConfigPath)) {
31
+ return { message: `Target "${targetRoot}" is not a ByteRover project (no .brv/config.json).`, success: false };
32
+ }
33
+ // 4. Not self
34
+ let canonicalProjectRoot;
35
+ try {
36
+ canonicalProjectRoot = realpathSync(projectRoot);
37
+ }
38
+ catch {
39
+ canonicalProjectRoot = projectRoot;
40
+ }
41
+ if (targetRoot === canonicalProjectRoot) {
42
+ return { message: 'Cannot add a source pointing to the current project.', success: false };
43
+ }
44
+ // 5. Read existing file — refuse to mutate if malformed
45
+ const existing = readSourcesFile(projectRoot);
46
+ if (existing.error) {
47
+ return { message: existing.error, success: false };
48
+ }
49
+ // 6. Not duplicate
50
+ const isDuplicate = existing.data.sources.some((source) => {
51
+ try {
52
+ return realpathSync(source.projectRoot) === targetRoot;
53
+ }
54
+ catch {
55
+ return source.projectRoot === targetRoot;
56
+ }
57
+ });
58
+ if (isDuplicate) {
59
+ return { message: `Source "${targetRoot}" already added.`, success: false };
60
+ }
61
+ // 7. Not circular
62
+ if (detectCircularSource(canonicalProjectRoot, targetRoot)) {
63
+ return {
64
+ message: `Circular source detected: "${basename(targetRoot)}" already references this project as a source.`,
65
+ success: false,
66
+ };
67
+ }
68
+ // 8. Derive alias — reject empty/whitespace-only
69
+ if (alias !== undefined && alias.trim() === '') {
70
+ return { message: 'Alias must not be empty.', success: false };
71
+ }
72
+ const derivedAlias = alias ?? basename(targetRoot);
73
+ // 9. Ensure alias uniqueness — append suffix if collision
74
+ const finalAlias = ensureUniqueAlias(derivedAlias, existing.data.sources);
75
+ // 10. Append and write
76
+ const newSource = {
77
+ addedAt: new Date().toISOString(),
78
+ alias: finalAlias,
79
+ projectRoot: targetRoot,
80
+ readOnly: true,
81
+ };
82
+ existing.data.sources.push(newSource);
83
+ writeSourcesFile(projectRoot, existing.data);
84
+ return { message: `Added source "${targetRoot}" as "${finalAlias}".`, success: true };
85
+ }
86
+ // ============================================================================
87
+ // Remove source
88
+ // ============================================================================
89
+ /**
90
+ * Removes a knowledge source by alias or path.
91
+ */
92
+ export function removeSource(projectRoot, aliasOrPath) {
93
+ const existing = readSourcesFile(projectRoot);
94
+ if (existing.error) {
95
+ return { message: existing.error, success: false };
96
+ }
97
+ if (existing.data.sources.length === 0) {
98
+ return { message: 'No knowledge sources configured.', success: false };
99
+ }
100
+ // Try match by alias first, then by canonical path
101
+ let matchIndex = existing.data.sources.findIndex((source) => source.alias === aliasOrPath);
102
+ if (matchIndex === -1) {
103
+ // Try matching by path
104
+ let canonicalTarget;
105
+ try {
106
+ canonicalTarget = realpathSync(aliasOrPath);
107
+ }
108
+ catch {
109
+ canonicalTarget = aliasOrPath;
110
+ }
111
+ matchIndex = existing.data.sources.findIndex((source) => {
112
+ try {
113
+ return realpathSync(source.projectRoot) === canonicalTarget;
114
+ }
115
+ catch {
116
+ return source.projectRoot === canonicalTarget;
117
+ }
118
+ });
119
+ }
120
+ if (matchIndex === -1) {
121
+ return { message: `No source found matching "${aliasOrPath}".`, success: false };
122
+ }
123
+ const removed = existing.data.sources.splice(matchIndex, 1)[0];
124
+ writeSourcesFile(projectRoot, existing.data);
125
+ return { message: `Removed source "${removed.alias}" (${removed.projectRoot}).`, success: true };
126
+ }
127
+ /**
128
+ * Returns status for all sources in the project.
129
+ * Surfaces malformed file errors instead of silently returning empty.
130
+ */
131
+ export function listSourceStatuses(projectRoot) {
132
+ const existing = readSourcesFile(projectRoot);
133
+ if (existing.error) {
134
+ return { error: existing.error, statuses: [] };
135
+ }
136
+ if (existing.data.sources.length === 0) {
137
+ return { statuses: [] };
138
+ }
139
+ return { statuses: getSourceStatuses(existing.data.sources) };
140
+ }
141
+ // ============================================================================
142
+ // Circular source detection
143
+ // ============================================================================
144
+ /**
145
+ * Checks if adding projectRoot → targetRoot would create a circular dependency.
146
+ * A circular reference exists if the target project already has a source pointing
147
+ * back to the current project (direct cycle only — no transitive check in v1).
148
+ */
149
+ export function detectCircularSource(projectRoot, targetRoot) {
150
+ const targetSources = loadSources(targetRoot);
151
+ if (!targetSources) {
152
+ return false;
153
+ }
154
+ return targetSources.sources.some((source) => {
155
+ try {
156
+ return realpathSync(source.projectRoot) === projectRoot;
157
+ }
158
+ catch {
159
+ return source.projectRoot === projectRoot;
160
+ }
161
+ });
162
+ }
163
+ function readSourcesFile(projectRoot) {
164
+ const filePath = join(projectRoot, BRV_DIR, SOURCES_FILE);
165
+ if (!existsSync(filePath)) {
166
+ return { data: { sources: [], version: 1 } };
167
+ }
168
+ let raw;
169
+ try {
170
+ raw = JSON.parse(readFileSync(filePath, 'utf8'));
171
+ }
172
+ catch {
173
+ return {
174
+ data: { sources: [], version: 1 },
175
+ error: `Malformed ${SOURCES_FILE}: file is not valid JSON. Back up or delete the file to recover.`,
176
+ };
177
+ }
178
+ const result = SourcesFileSchema.safeParse(raw);
179
+ if (!result.success) {
180
+ return {
181
+ data: { sources: [], version: 1 },
182
+ error: `Malformed ${SOURCES_FILE}: schema validation failed. Back up or delete the file to recover.`,
183
+ };
184
+ }
185
+ return { data: result.data };
186
+ }
187
+ function writeSourcesFile(projectRoot, data) {
188
+ const filePath = join(projectRoot, BRV_DIR, SOURCES_FILE);
189
+ writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
190
+ }
191
+ function ensureUniqueAlias(baseAlias, existingSources) {
192
+ const existingAliases = new Set(existingSources.map((source) => source.alias));
193
+ if (!existingAliases.has(baseAlias)) {
194
+ return baseAlias;
195
+ }
196
+ let counter = 2;
197
+ while (existingAliases.has(`${baseAlias}-${counter}`)) {
198
+ counter++;
199
+ }
200
+ return `${baseAlias}-${counter}`;
201
+ }
@@ -0,0 +1,94 @@
1
+ import { z } from 'zod';
2
+ export declare const SourceSchema: z.ZodObject<{
3
+ addedAt: z.ZodString;
4
+ alias: z.ZodString;
5
+ projectRoot: z.ZodString;
6
+ readOnly: z.ZodLiteral<true>;
7
+ }, "strip", z.ZodTypeAny, {
8
+ addedAt: string;
9
+ alias: string;
10
+ projectRoot: string;
11
+ readOnly: true;
12
+ }, {
13
+ addedAt: string;
14
+ alias: string;
15
+ projectRoot: string;
16
+ readOnly: true;
17
+ }>;
18
+ export declare const SourcesFileSchema: z.ZodObject<{
19
+ sources: z.ZodArray<z.ZodObject<{
20
+ addedAt: z.ZodString;
21
+ alias: z.ZodString;
22
+ projectRoot: z.ZodString;
23
+ readOnly: z.ZodLiteral<true>;
24
+ }, "strip", z.ZodTypeAny, {
25
+ addedAt: string;
26
+ alias: string;
27
+ projectRoot: string;
28
+ readOnly: true;
29
+ }, {
30
+ addedAt: string;
31
+ alias: string;
32
+ projectRoot: string;
33
+ readOnly: true;
34
+ }>, "many">;
35
+ version: z.ZodLiteral<1>;
36
+ }, "strip", z.ZodTypeAny, {
37
+ version: 1;
38
+ sources: {
39
+ addedAt: string;
40
+ alias: string;
41
+ projectRoot: string;
42
+ readOnly: true;
43
+ }[];
44
+ }, {
45
+ version: 1;
46
+ sources: {
47
+ addedAt: string;
48
+ alias: string;
49
+ projectRoot: string;
50
+ readOnly: true;
51
+ }[];
52
+ }>;
53
+ export type Source = z.infer<typeof SourceSchema>;
54
+ export type SourcesFile = z.infer<typeof SourcesFileSchema>;
55
+ export interface SearchOrigin {
56
+ alias?: string;
57
+ contextTreeRoot: string;
58
+ origin: 'local' | 'shared';
59
+ originKey: string;
60
+ }
61
+ /**
62
+ * Derives a stable, short origin key from a canonical path.
63
+ * Uses first 12 hex chars of SHA-256 to avoid alias-based collisions.
64
+ */
65
+ export declare function deriveOriginKey(canonicalPath: string): string;
66
+ export interface LoadedSources {
67
+ mtime: number;
68
+ /** Search origins derived from valid sources (callers can search them) */
69
+ origins: SearchOrigin[];
70
+ /** All configured sources (including broken ones — for status display) */
71
+ sources: Source[];
72
+ }
73
+ /**
74
+ * Loads and validates `.brv/sources.json` from a project root.
75
+ *
76
+ * Returns null if the file does not exist.
77
+ * Broken sources (target `.brv/` missing) are included in `sources` but excluded
78
+ * from `origins` — callers decide how to surface them (status vs search).
79
+ */
80
+ export declare function loadSources(projectRoot: string): LoadedSources | null;
81
+ export interface SourceStatus {
82
+ alias: string;
83
+ contextTreeSize?: number;
84
+ projectRoot: string;
85
+ valid: boolean;
86
+ }
87
+ /**
88
+ * Validates each source and returns status for display.
89
+ *
90
+ * A source is valid only when both `.brv/config.json` AND `.brv/context-tree/`
91
+ * exist — matching what loadSources() requires before including a source in
92
+ * search origins. When valid, `contextTreeSize` counts `.md` files.
93
+ */
94
+ export declare function getSourceStatuses(sources: Source[]): SourceStatus[];
@@ -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,24 +515,28 @@ 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;
521
523
  taskId: string;
522
524
  clientId: string;
523
525
  files?: string[] | undefined;
524
- projectPath?: string | undefined;
525
- folderPath?: string | undefined;
526
526
  clientCwd?: string | undefined;
527
+ folderPath?: string | undefined;
528
+ projectPath?: string | undefined;
529
+ worktreeRoot?: string | undefined;
527
530
  }, {
528
531
  type: "curate" | "query" | "curate-folder";
529
532
  content: string;
530
533
  taskId: string;
531
534
  clientId: string;
532
535
  files?: string[] | undefined;
533
- projectPath?: string | undefined;
534
- folderPath?: string | undefined;
535
536
  clientCwd?: string | undefined;
537
+ folderPath?: string | undefined;
538
+ projectPath?: string | undefined;
539
+ worktreeRoot?: string | undefined;
536
540
  }>;
537
541
  /**
538
542
  * task:cancel - Transport tells Agent to cancel a task
@@ -689,15 +693,15 @@ export declare const TaskCreatedSchema: z.ZodObject<{
689
693
  content: string;
690
694
  taskId: string;
691
695
  files?: string[] | undefined;
692
- folderPath?: string | undefined;
693
696
  clientCwd?: string | undefined;
697
+ folderPath?: string | undefined;
694
698
  }, {
695
699
  type: "curate" | "query" | "curate-folder";
696
700
  content: string;
697
701
  taskId: string;
698
702
  files?: string[] | undefined;
699
- folderPath?: string | undefined;
700
703
  clientCwd?: string | undefined;
704
+ folderPath?: string | undefined;
701
705
  }>;
702
706
  /**
703
707
  * task:started - Agent begins processing the task
@@ -960,22 +964,26 @@ 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;
966
972
  taskId: string;
967
973
  files?: string[] | undefined;
968
- projectPath?: string | undefined;
969
- folderPath?: string | undefined;
970
974
  clientCwd?: string | undefined;
975
+ folderPath?: string | undefined;
976
+ projectPath?: string | undefined;
977
+ worktreeRoot?: string | undefined;
971
978
  }, {
972
979
  type: "curate" | "query" | "curate-folder";
973
980
  content: string;
974
981
  taskId: string;
975
982
  files?: string[] | undefined;
976
- projectPath?: string | undefined;
977
- folderPath?: string | undefined;
978
983
  clientCwd?: string | undefined;
984
+ folderPath?: string | undefined;
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) {
@@ -6,7 +6,7 @@
6
6
  * - isArchiveStub() — searchable stubs (included in BM25 index and fingerprint)
7
7
  * - isExcludedFromSync() — union of above (excluded from snapshot/sync/merge/push)
8
8
  */
9
- import { ARCHIVE_DIR, FULL_ARCHIVE_EXTENSION, MANIFEST_FILE, STUB_EXTENSION, SUMMARY_INDEX_FILE } from '../../constants.js';
9
+ import { ABSTRACT_EXTENSION, ARCHIVE_DIR, FULL_ARCHIVE_EXTENSION, MANIFEST_FILE, OVERVIEW_EXTENSION, STUB_EXTENSION, SUMMARY_INDEX_FILE } from '../../constants.js';
10
10
  import { toUnixPath } from './path-utils.js';
11
11
  /**
12
12
  * Returns true if the given relative path is a derived artifact
@@ -24,6 +24,10 @@ export function isDerivedArtifact(relativePath) {
24
24
  return true;
25
25
  if (fileName === MANIFEST_FILE)
26
26
  return true;
27
+ if (fileName.endsWith(ABSTRACT_EXTENSION))
28
+ return true;
29
+ if (fileName.endsWith(OVERVIEW_EXTENSION))
30
+ return true;
27
31
  if (segments.includes(ARCHIVE_DIR) && fileName.endsWith(FULL_ARCHIVE_EXTENSION))
28
32
  return true;
29
33
  return false;
@@ -44,7 +44,8 @@ export declare class FileContextTreeManifestService implements IContextTreeManif
44
44
  private scanForManifest;
45
45
  /**
46
46
  * Recursively collect stat data for all source files (for fingerprint).
47
- * Excludes derived artifacts.
47
+ * Excludes derived artifacts except .abstract.md siblings, which are included
48
+ * so abstract generation invalidates the manifest without a second tree walk.
48
49
  */
49
50
  private scanSourceStats;
50
51
  }