byterover-cli 3.2.0 → 3.4.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 (127) hide show
  1. package/.env.production +0 -4
  2. package/dist/agent/core/domain/swarm/types.d.ts +132 -0
  3. package/dist/agent/core/domain/swarm/types.js +128 -0
  4. package/dist/agent/core/domain/tools/constants.d.ts +2 -0
  5. package/dist/agent/core/domain/tools/constants.js +2 -0
  6. package/dist/agent/core/interfaces/i-memory-provider.d.ts +45 -0
  7. package/dist/agent/core/interfaces/i-memory-provider.js +1 -0
  8. package/dist/agent/core/interfaces/i-sandbox-service.d.ts +8 -0
  9. package/dist/agent/core/interfaces/i-swarm-coordinator.d.ts +127 -0
  10. package/dist/agent/core/interfaces/i-swarm-coordinator.js +1 -0
  11. package/dist/agent/infra/agent/service-initializer.js +48 -0
  12. package/dist/agent/infra/map/map-shared.d.ts +2 -2
  13. package/dist/agent/infra/sandbox/sandbox-service.d.ts +10 -0
  14. package/dist/agent/infra/sandbox/sandbox-service.js +13 -0
  15. package/dist/agent/infra/sandbox/tools-sdk.d.ts +25 -0
  16. package/dist/agent/infra/sandbox/tools-sdk.js +24 -1
  17. package/dist/agent/infra/swarm/adapters/byterover-adapter.d.ts +39 -0
  18. package/dist/agent/infra/swarm/adapters/byterover-adapter.js +62 -0
  19. package/dist/agent/infra/swarm/adapters/gbrain-adapter.d.ts +63 -0
  20. package/dist/agent/infra/swarm/adapters/gbrain-adapter.js +209 -0
  21. package/dist/agent/infra/swarm/adapters/local-markdown-adapter.d.ts +41 -0
  22. package/dist/agent/infra/swarm/adapters/local-markdown-adapter.js +256 -0
  23. package/dist/agent/infra/swarm/adapters/memory-wiki-adapter.d.ts +29 -0
  24. package/dist/agent/infra/swarm/adapters/memory-wiki-adapter.js +244 -0
  25. package/dist/agent/infra/swarm/adapters/obsidian-adapter.d.ts +37 -0
  26. package/dist/agent/infra/swarm/adapters/obsidian-adapter.js +201 -0
  27. package/dist/agent/infra/swarm/cli/query-renderer.d.ts +15 -0
  28. package/dist/agent/infra/swarm/cli/query-renderer.js +126 -0
  29. package/dist/agent/infra/swarm/config/swarm-config-loader.d.ts +14 -0
  30. package/dist/agent/infra/swarm/config/swarm-config-loader.js +82 -0
  31. package/dist/agent/infra/swarm/config/swarm-config-schema.d.ts +667 -0
  32. package/dist/agent/infra/swarm/config/swarm-config-schema.js +305 -0
  33. package/dist/agent/infra/swarm/provider-factory.d.ts +21 -0
  34. package/dist/agent/infra/swarm/provider-factory.js +67 -0
  35. package/dist/agent/infra/swarm/search-precision.d.ts +95 -0
  36. package/dist/agent/infra/swarm/search-precision.js +141 -0
  37. package/dist/agent/infra/swarm/swarm-coordinator.d.ts +59 -0
  38. package/dist/agent/infra/swarm/swarm-coordinator.js +436 -0
  39. package/dist/agent/infra/swarm/swarm-graph.d.ts +63 -0
  40. package/dist/agent/infra/swarm/swarm-graph.js +167 -0
  41. package/dist/agent/infra/swarm/swarm-merger.d.ts +29 -0
  42. package/dist/agent/infra/swarm/swarm-merger.js +66 -0
  43. package/dist/agent/infra/swarm/swarm-router.d.ts +12 -0
  44. package/dist/agent/infra/swarm/swarm-router.js +40 -0
  45. package/dist/agent/infra/swarm/swarm-write-router.d.ts +23 -0
  46. package/dist/agent/infra/swarm/swarm-write-router.js +45 -0
  47. package/dist/agent/infra/swarm/validation/config-validator.d.ts +16 -0
  48. package/dist/agent/infra/swarm/validation/config-validator.js +402 -0
  49. package/dist/agent/infra/swarm/validation/memory-swarm-validation-error.d.ts +33 -0
  50. package/dist/agent/infra/swarm/validation/memory-swarm-validation-error.js +27 -0
  51. package/dist/agent/infra/swarm/wizard/config-scaffolder.d.ts +36 -0
  52. package/dist/agent/infra/swarm/wizard/config-scaffolder.js +96 -0
  53. package/dist/agent/infra/swarm/wizard/provider-detector.d.ts +54 -0
  54. package/dist/agent/infra/swarm/wizard/provider-detector.js +153 -0
  55. package/dist/agent/infra/swarm/wizard/swarm-wizard.d.ts +61 -0
  56. package/dist/agent/infra/swarm/wizard/swarm-wizard.js +187 -0
  57. package/dist/agent/infra/system-prompt/contributors/index.d.ts +1 -0
  58. package/dist/agent/infra/system-prompt/contributors/index.js +1 -0
  59. package/dist/agent/infra/system-prompt/contributors/swarm-state-contributor.d.ts +15 -0
  60. package/dist/agent/infra/system-prompt/contributors/swarm-state-contributor.js +65 -0
  61. package/dist/agent/infra/tools/implementations/curate-tool.d.ts +14 -14
  62. package/dist/agent/infra/tools/implementations/curate-tool.js +2 -0
  63. package/dist/agent/infra/tools/implementations/search-knowledge-service.js +12 -2
  64. package/dist/agent/infra/tools/implementations/swarm-query-tool.d.ts +9 -0
  65. package/dist/agent/infra/tools/implementations/swarm-query-tool.js +44 -0
  66. package/dist/agent/infra/tools/implementations/swarm-store-tool.d.ts +9 -0
  67. package/dist/agent/infra/tools/implementations/swarm-store-tool.js +43 -0
  68. package/dist/agent/infra/tools/tool-provider.js +1 -0
  69. package/dist/agent/infra/tools/tool-registry.d.ts +3 -0
  70. package/dist/agent/infra/tools/tool-registry.js +25 -1
  71. package/dist/agent/resources/tools/code_exec.txt +2 -0
  72. package/dist/agent/resources/tools/swarm_query.txt +38 -0
  73. package/dist/agent/resources/tools/swarm_store.txt +35 -0
  74. package/dist/oclif/commands/curate/index.d.ts +1 -0
  75. package/dist/oclif/commands/curate/index.js +15 -1
  76. package/dist/oclif/commands/query.d.ts +1 -0
  77. package/dist/oclif/commands/query.js +17 -3
  78. package/dist/oclif/commands/search.d.ts +20 -0
  79. package/dist/oclif/commands/search.js +186 -0
  80. package/dist/oclif/commands/status.js +4 -0
  81. package/dist/oclif/commands/swarm/curate.d.ts +13 -0
  82. package/dist/oclif/commands/swarm/curate.js +81 -0
  83. package/dist/oclif/commands/swarm/onboard.d.ts +6 -0
  84. package/dist/oclif/commands/swarm/onboard.js +233 -0
  85. package/dist/oclif/commands/swarm/query.d.ts +14 -0
  86. package/dist/oclif/commands/swarm/query.js +84 -0
  87. package/dist/oclif/commands/swarm/status.d.ts +41 -0
  88. package/dist/oclif/commands/swarm/status.js +278 -0
  89. package/dist/oclif/lib/daemon-client.js +0 -1
  90. package/dist/oclif/lib/search-format.d.ts +10 -0
  91. package/dist/oclif/lib/search-format.js +25 -0
  92. package/dist/oclif/lib/task-client.d.ts +6 -0
  93. package/dist/oclif/lib/task-client.js +10 -3
  94. package/dist/server/constants.d.ts +3 -2
  95. package/dist/server/constants.js +10 -7
  96. package/dist/server/core/domain/errors/task-error.d.ts +2 -2
  97. package/dist/server/core/domain/errors/task-error.js +5 -4
  98. package/dist/server/core/domain/source/source-schema.d.ts +6 -6
  99. package/dist/server/core/domain/transport/schemas.d.ts +14 -14
  100. package/dist/server/core/domain/transport/schemas.js +3 -3
  101. package/dist/server/core/interfaces/executor/i-search-executor.d.ts +34 -0
  102. package/dist/server/core/interfaces/executor/i-search-executor.js +1 -0
  103. package/dist/server/core/interfaces/executor/index.d.ts +1 -0
  104. package/dist/server/core/interfaces/executor/index.js +1 -0
  105. package/dist/server/infra/daemon/agent-process.js +20 -7
  106. package/dist/server/infra/executor/search-executor.d.ts +17 -0
  107. package/dist/server/infra/executor/search-executor.js +30 -0
  108. package/dist/server/infra/http/provider-model-fetchers.js +1 -0
  109. package/dist/server/infra/process/feature-handlers.js +13 -0
  110. package/dist/server/infra/project/project-registry.js +13 -1
  111. package/dist/server/infra/transport/handlers/locations-handler.d.ts +2 -0
  112. package/dist/server/infra/transport/handlers/locations-handler.js +16 -1
  113. package/dist/server/infra/transport/handlers/pull-handler.js +3 -3
  114. package/dist/server/infra/transport/handlers/push-handler.js +3 -3
  115. package/dist/server/infra/transport/handlers/status-handler.js +25 -18
  116. package/dist/server/infra/transport/handlers/vc-handler.d.ts +0 -4
  117. package/dist/server/infra/transport/handlers/vc-handler.js +5 -16
  118. package/dist/server/templates/skill/SKILL.md +188 -5
  119. package/dist/server/utils/gitignore.d.ts +1 -0
  120. package/dist/server/utils/gitignore.js +36 -4
  121. package/dist/shared/transport/search-content.d.ts +28 -0
  122. package/dist/shared/transport/search-content.js +38 -0
  123. package/dist/shared/transport/types/dto.d.ts +1 -1
  124. package/dist/tui/features/status/utils/format-status.js +5 -0
  125. package/dist/tui/utils/error-messages.js +2 -2
  126. package/oclif.manifest.json +581 -317
  127. package/package.json +2 -2
@@ -0,0 +1,34 @@
1
+ import type { SearchKnowledgeResult } from '../../../../agent/infra/sandbox/tools-sdk.js';
2
+ /**
3
+ * Options for executing a context tree search.
4
+ * Search is stateless — no agent session, no LLM, no task isolation needed.
5
+ */
6
+ export interface SearchExecuteOptions {
7
+ /** Maximum number of results to return (default: 10) */
8
+ limit?: number;
9
+ /** Search query */
10
+ query: string;
11
+ /** Path prefix to scope results (e.g. "auth/" for auth domain only) */
12
+ scope?: string;
13
+ }
14
+ /**
15
+ * ISearchExecutor - Executes search against the context tree's BM25 index.
16
+ *
17
+ * Unlike QueryExecutor (which requires a CipherAgent for LLM synthesis),
18
+ * SearchExecutor is stateless and returns raw search results directly.
19
+ * No agent session, no sandbox, no LLM call.
20
+ *
21
+ * Architecture:
22
+ * - SearchKnowledgeService provides BM25-indexed search
23
+ * - Executor wraps it with option validation
24
+ * - Results are SearchKnowledgeResult (paths, scores, excerpts)
25
+ */
26
+ export interface ISearchExecutor {
27
+ /**
28
+ * Execute a context tree search.
29
+ *
30
+ * @param options - Search options (query, limit, scope)
31
+ * @returns Raw search results with paths, scores, and excerpts
32
+ */
33
+ execute(options: SearchExecuteOptions): Promise<SearchKnowledgeResult>;
34
+ }
@@ -1,3 +1,4 @@
1
1
  export * from './i-curate-executor.js';
2
2
  export * from './i-folder-pack-executor.js';
3
3
  export * from './i-query-executor.js';
4
+ export * from './i-search-executor.js';
@@ -1,3 +1,4 @@
1
1
  export * from './i-curate-executor.js';
2
2
  export * from './i-folder-pack-executor.js';
3
3
  export * from './i-query-executor.js';
4
+ export * from './i-search-executor.js';
@@ -28,6 +28,7 @@ import { FolderPackService } from '../../../agent/infra/folder-pack/folder-pack-
28
28
  import { SessionMetadataStore } from '../../../agent/infra/session/session-metadata-store.js';
29
29
  import { createSearchKnowledgeService } from '../../../agent/infra/tools/implementations/search-knowledge-service.js';
30
30
  import { AuthEvents } from '../../../shared/transport/events/auth-events.js';
31
+ import { decodeSearchContent } from '../../../shared/transport/search-content.js';
31
32
  import { getCurrentConfig } from '../../config/environment.js';
32
33
  import { DEFAULT_LLM_MODEL, PROJECT } from '../../constants.js';
33
34
  import { serializeTaskError, TaskError, TaskErrorCode } from '../../core/domain/errors/task-error.js';
@@ -36,6 +37,7 @@ import { TransportAgentEventNames, TransportDaemonEventNames, TransportStateEven
36
37
  import { CurateExecutor } from '../executor/curate-executor.js';
37
38
  import { FolderPackExecutor } from '../executor/folder-pack-executor.js';
38
39
  import { QueryExecutor } from '../executor/query-executor.js';
40
+ import { SearchExecutor } from '../executor/search-executor.js';
39
41
  import { AgentInstanceDiscovery } from '../transport/agent-instance-discovery.js';
40
42
  import { createAgentLogger } from './agent-logger.js';
41
43
  import { resolveSessionId } from './session-resolver.js';
@@ -301,10 +303,11 @@ async function start() {
301
303
  fileSystem: fileSystemService,
302
304
  searchService,
303
305
  });
306
+ const searchExecutor = new SearchExecutor(searchService);
304
307
  transport.on(TransportTaskEventNames.EXECUTE, (task) => {
305
308
  agentLog(`task:execute received taskId=${task.taskId} type=${task.type} activeTaskCount=${activeTaskCount + 1}`);
306
309
  // eslint-disable-next-line no-void
307
- void executeTask(task, curateExecutor, folderPackExecutor, queryExecutor);
310
+ void executeTask(task, curateExecutor, folderPackExecutor, queryExecutor, searchExecutor);
308
311
  });
309
312
  // 8. Register with transport server (for TransportHandlers tracking)
310
313
  await transport.requestWithAck('agent:register', { projectPath });
@@ -312,15 +315,19 @@ async function start() {
312
315
  process.send?.({ clientId, type: 'ready' });
313
316
  agentLog('Ready — listening for tasks');
314
317
  }
315
- async function executeTask(task, curateExecutor, folderPackExecutor, queryExecutor) {
318
+ async function executeTask(task, curateExecutor, folderPackExecutor, queryExecutor, searchExecutor) {
316
319
  const { clientCwd, clientId, content, files, folderPath, taskId, type, worktreeRoot } = task;
317
320
  if (!transport || !agent)
318
321
  return;
319
- const freshProviderConfig = await transport.requestWithAck(TransportStateEventNames.GET_PROVIDER_CONFIG);
320
- const validationError = validateProviderForTask(freshProviderConfig);
321
- if (validationError) {
322
- transport.request(TransportTaskEventNames.ERROR, { clientId, error: validationError, taskId });
323
- return;
322
+ // Search tasks are pure BM25 retrieval — no LLM, no provider needed.
323
+ // Skip provider validation so search works even without a configured provider.
324
+ if (type !== 'search') {
325
+ const freshProviderConfig = await transport.requestWithAck(TransportStateEventNames.GET_PROVIDER_CONFIG);
326
+ const validationError = validateProviderForTask(freshProviderConfig);
327
+ if (validationError) {
328
+ transport.request(TransportTaskEventNames.ERROR, { clientId, error: validationError, taskId });
329
+ return;
330
+ }
324
331
  }
325
332
  activeTaskCount++;
326
333
  try {
@@ -398,6 +405,12 @@ async function executeTask(task, curateExecutor, folderPackExecutor, queryExecut
398
405
  result = await queryExecutor.executeWithAgent(agent, { query: content, taskId, worktreeRoot });
399
406
  break;
400
407
  }
408
+ case 'search': {
409
+ const searchOptions = decodeSearchContent(content);
410
+ const searchResult = await searchExecutor.execute(searchOptions);
411
+ result = JSON.stringify(searchResult);
412
+ break;
413
+ }
401
414
  }
402
415
  // Emit task:completed
403
416
  agentLog(`task:completed taskId=${taskId}`);
@@ -0,0 +1,17 @@
1
+ /**
2
+ * SearchExecutor - Executes context tree searches via SearchKnowledgeService.
3
+ *
4
+ * Unlike QueryExecutor (Tier 0-4 with LLM synthesis), SearchExecutor is
5
+ * pure retrieval: BM25 index lookup → scored results. No LLM, no agent
6
+ * session, no sandbox, no token cost.
7
+ *
8
+ * This is the engine behind `brv search`. The CLI command and transport
9
+ * layer handle I/O; this module handles the search logic.
10
+ */
11
+ import type { ISearchKnowledgeService, SearchKnowledgeResult } from '../../../agent/infra/sandbox/tools-sdk.js';
12
+ import type { ISearchExecutor, SearchExecuteOptions } from '../../core/interfaces/executor/i-search-executor.js';
13
+ export declare class SearchExecutor implements ISearchExecutor {
14
+ private readonly searchService;
15
+ constructor(searchService: ISearchKnowledgeService);
16
+ execute(options: SearchExecuteOptions): Promise<SearchKnowledgeResult>;
17
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * SearchExecutor - Executes context tree searches via SearchKnowledgeService.
3
+ *
4
+ * Unlike QueryExecutor (Tier 0-4 with LLM synthesis), SearchExecutor is
5
+ * pure retrieval: BM25 index lookup → scored results. No LLM, no agent
6
+ * session, no sandbox, no token cost.
7
+ *
8
+ * This is the engine behind `brv search`. The CLI command and transport
9
+ * layer handle I/O; this module handles the search logic.
10
+ */
11
+ const DEFAULT_LIMIT = 10;
12
+ const MAX_LIMIT = 50;
13
+ export class SearchExecutor {
14
+ searchService;
15
+ constructor(searchService) {
16
+ this.searchService = searchService;
17
+ }
18
+ async execute(options) {
19
+ const query = options.query.trim();
20
+ if (!query) {
21
+ return { message: 'Empty query', results: [], totalFound: 0 };
22
+ }
23
+ const scope = options.scope?.trim() || undefined;
24
+ const limit = Math.min(MAX_LIMIT, Math.max(1, Math.trunc(options.limit ?? DEFAULT_LIMIT)));
25
+ return this.searchService.search(query, {
26
+ limit,
27
+ ...(scope ? { scope } : {}),
28
+ });
29
+ }
30
+ }
@@ -121,6 +121,7 @@ export const CODEX_ALLOWED_MODELS = new Set([
121
121
  'gpt-5.2-codex',
122
122
  'gpt-5.3-codex',
123
123
  'gpt-5.4',
124
+ 'gpt-5.4-mini',
124
125
  ]);
125
126
  /**
126
127
  * Fallback Codex models used when models.dev is unreachable and no disk cache exists.
@@ -4,6 +4,7 @@
4
4
  * Registers all feature handlers (auth, init, status, etc.) on the transport server.
5
5
  * These handlers implement the TUI ↔ Server event contract.
6
6
  */
7
+ import { access } from 'node:fs/promises';
7
8
  import { join } from 'node:path';
8
9
  import { ReviewEvents } from '../../../shared/transport/events/review-events.js';
9
10
  import { getAuthConfig } from '../../config/auth.config.js';
@@ -106,6 +107,18 @@ export async function setupFeatureHandlers({ authStateStore, broadcastToProject,
106
107
  new LocationsHandler({
107
108
  contextTreeService,
108
109
  getActiveProjectPaths,
110
+ async pathExists(path) {
111
+ try {
112
+ await access(path);
113
+ return true;
114
+ }
115
+ catch (error) {
116
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
117
+ return false;
118
+ }
119
+ throw error;
120
+ }
121
+ },
109
122
  projectRegistry,
110
123
  resolveProjectPath,
111
124
  transport,
@@ -82,7 +82,19 @@ export class ProjectRegistry {
82
82
  return info;
83
83
  }
84
84
  unregister(projectPath) {
85
- const resolved = resolvePath(projectPath);
85
+ let resolved;
86
+ try {
87
+ resolved = resolvePath(projectPath);
88
+ }
89
+ catch (error) {
90
+ // ENOENT — path deleted from disk. Keys are already resolved, so try the raw input.
91
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
92
+ resolved = projectPath;
93
+ }
94
+ else {
95
+ throw error;
96
+ }
97
+ }
86
98
  if (!this.projects.has(resolved)) {
87
99
  return false;
88
100
  }
@@ -5,6 +5,7 @@ import { type ProjectPathResolver } from './handler-types.js';
5
5
  export interface LocationsHandlerDeps {
6
6
  contextTreeService: IContextTreeService;
7
7
  getActiveProjectPaths: () => string[];
8
+ pathExists: (path: string) => Promise<boolean>;
8
9
  projectRegistry: IProjectRegistry;
9
10
  resolveProjectPath: ProjectPathResolver;
10
11
  transport: ITransportServer;
@@ -16,6 +17,7 @@ export interface LocationsHandlerDeps {
16
17
  export declare class LocationsHandler {
17
18
  private readonly contextTreeService;
18
19
  private readonly getActiveProjectPaths;
20
+ private readonly pathExists;
19
21
  private readonly projectRegistry;
20
22
  private readonly resolveProjectPath;
21
23
  private readonly transport;
@@ -9,12 +9,14 @@ import { resolveRequiredProjectPath } from './handler-types.js';
9
9
  export class LocationsHandler {
10
10
  contextTreeService;
11
11
  getActiveProjectPaths;
12
+ pathExists;
12
13
  projectRegistry;
13
14
  resolveProjectPath;
14
15
  transport;
15
16
  constructor(deps) {
16
17
  this.contextTreeService = deps.contextTreeService;
17
18
  this.getActiveProjectPaths = deps.getActiveProjectPaths;
19
+ this.pathExists = deps.pathExists;
18
20
  this.projectRegistry = deps.projectRegistry;
19
21
  this.resolveProjectPath = deps.resolveProjectPath;
20
22
  this.transport = deps.transport;
@@ -35,6 +37,18 @@ export class LocationsHandler {
35
37
  const all = this.projectRegistry.getAll();
36
38
  const activeSet = new Set(this.getActiveProjectPaths());
37
39
  const results = await Promise.all([...all.entries()].map(async ([path]) => {
40
+ try {
41
+ const exists = await this.pathExists(path);
42
+ if (!exists) {
43
+ this.projectRegistry.unregister(path);
44
+ return null;
45
+ }
46
+ }
47
+ catch {
48
+ // pathExists threw unexpectedly — skip this entry but do NOT unregister,
49
+ // since the path may still exist (e.g. a transient permission error).
50
+ return null;
51
+ }
38
52
  let isInitialized = false;
39
53
  try {
40
54
  isInitialized = await this.contextTreeService.exists(path);
@@ -50,8 +64,9 @@ export class LocationsHandler {
50
64
  projectPath: path,
51
65
  };
52
66
  }));
67
+ const filtered = results.filter((r) => r !== null);
53
68
  // Sort: current first → active (has clients) → initialized → rest, all by registeredAt desc
54
- return results.sort((a, b) => {
69
+ return filtered.sort((a, b) => {
55
70
  if (a.isCurrent !== b.isCurrent)
56
71
  return a.isCurrent ? -1 : 1;
57
72
  if (a.isActive !== b.isActive)
@@ -1,5 +1,5 @@
1
1
  import { PullEvents, } from '../../../../shared/transport/events/pull-events.js';
2
- import { LocalChangesExistError, NotAuthenticatedError, ProjectNotInitError, SpaceNotConfiguredError, } from '../../../core/domain/errors/task-error.js';
2
+ import { LegacySyncUnavailableError, LocalChangesExistError, NotAuthenticatedError, ProjectNotInitError, } from '../../../core/domain/errors/task-error.js';
3
3
  import { guardAgainstGitVc, hasAnyChanges, resolveRequiredProjectPath, } from './handler-types.js';
4
4
  /**
5
5
  * Handles pull:* events.
@@ -42,7 +42,7 @@ export class PullHandler {
42
42
  throw new ProjectNotInitError();
43
43
  }
44
44
  if (!config.teamId || !config.spaceId) {
45
- throw new SpaceNotConfiguredError();
45
+ throw new LegacySyncUnavailableError();
46
46
  }
47
47
  // Check for local changes that would be overwritten
48
48
  const changes = await this.contextTreeSnapshotService.getChanges(projectPath);
@@ -79,7 +79,7 @@ export class PullHandler {
79
79
  throw new ProjectNotInitError();
80
80
  }
81
81
  if (!config.teamId || !config.spaceId) {
82
- throw new SpaceNotConfiguredError();
82
+ throw new LegacySyncUnavailableError();
83
83
  }
84
84
  const changes = await this.contextTreeSnapshotService.getChanges(projectPath);
85
85
  const hasLocalChanges = hasAnyChanges(changes);
@@ -1,6 +1,6 @@
1
1
  import { join, relative } from 'node:path';
2
2
  import { PushEvents, } from '../../../../shared/transport/events/push-events.js';
3
- import { NotAuthenticatedError, ProjectNotInitError, SpaceNotConfiguredError, } from '../../../core/domain/errors/task-error.js';
3
+ import { LegacySyncUnavailableError, NotAuthenticatedError, ProjectNotInitError, } from '../../../core/domain/errors/task-error.js';
4
4
  import { mapToPushContexts } from '../../cogit/context-tree-to-push-context-mapper.js';
5
5
  import { guardAgainstGitVc, resolveRequiredProjectPath, } from './handler-types.js';
6
6
  /** Path prefix of the context tree relative to the project root. */
@@ -88,7 +88,7 @@ export class PushHandler {
88
88
  throw new ProjectNotInitError();
89
89
  }
90
90
  if (!config.teamId || !config.spaceId) {
91
- throw new SpaceNotConfiguredError();
91
+ throw new LegacySyncUnavailableError();
92
92
  }
93
93
  this.broadcastToProject(projectPath, PushEvents.PROGRESS, { message: 'Reading context files...', step: 'reading' });
94
94
  const changes = await this.contextTreeSnapshotService.getChanges(projectPath);
@@ -163,7 +163,7 @@ export class PushHandler {
163
163
  throw new ProjectNotInitError();
164
164
  }
165
165
  if (!config.teamId || !config.spaceId) {
166
- throw new SpaceNotConfiguredError();
166
+ throw new LegacySyncUnavailableError();
167
167
  }
168
168
  const hasSnapshot = await this.contextTreeSnapshotService.hasSnapshot(projectPath);
169
169
  if (!hasSnapshot) {
@@ -84,13 +84,14 @@ export class StatusHandler {
84
84
  result.authStatus = 'unknown';
85
85
  }
86
86
  // Project status — use effectiveProjectPath for consistency with resolved root
87
+ let projectConfig;
87
88
  try {
88
89
  const isInitialized = await this.projectConfigStore.exists(effectiveProjectPath);
89
90
  if (isInitialized) {
90
- const config = await this.projectConfigStore.read(effectiveProjectPath);
91
- if (config) {
92
- result.teamName = config.teamName;
93
- result.spaceName = config.spaceName;
91
+ projectConfig = await this.projectConfigStore.read(effectiveProjectPath);
92
+ if (projectConfig) {
93
+ result.teamName = projectConfig.teamName;
94
+ result.spaceName = projectConfig.spaceName;
94
95
  }
95
96
  }
96
97
  }
@@ -115,22 +116,28 @@ export class StatusHandler {
115
116
  else {
116
117
  result.contextTreeDir = join(effectiveProjectPath, BRV_DIR, CONTEXT_TREE_DIR);
117
118
  result.contextTreeRelativeDir = join(BRV_DIR, CONTEXT_TREE_DIR);
118
- const hasSnapshot = await this.contextTreeSnapshotService.hasSnapshot(effectiveProjectPath);
119
- if (!hasSnapshot) {
120
- await this.contextTreeSnapshotService.initEmptySnapshot(effectiveProjectPath);
121
- }
122
- const changes = await this.contextTreeSnapshotService.getChanges(effectiveProjectPath);
123
- const hasChanges = changes.added.length > 0 || changes.modified.length > 0 || changes.deleted.length > 0;
124
- if (hasChanges) {
125
- result.contextTreeStatus = 'has_changes';
126
- result.contextTreeChanges = {
127
- added: changes.added,
128
- deleted: changes.deleted,
129
- modified: changes.modified,
130
- };
119
+ const hasLegacySyncConfig = Boolean(projectConfig?.teamId && projectConfig?.spaceId);
120
+ if (hasLegacySyncConfig) {
121
+ const hasSnapshot = await this.contextTreeSnapshotService.hasSnapshot(effectiveProjectPath);
122
+ if (!hasSnapshot) {
123
+ await this.contextTreeSnapshotService.initEmptySnapshot(effectiveProjectPath);
124
+ }
125
+ const changes = await this.contextTreeSnapshotService.getChanges(effectiveProjectPath);
126
+ const hasChanges = changes.added.length > 0 || changes.modified.length > 0 || changes.deleted.length > 0;
127
+ if (hasChanges) {
128
+ result.contextTreeStatus = 'has_changes';
129
+ result.contextTreeChanges = {
130
+ added: changes.added,
131
+ deleted: changes.deleted,
132
+ modified: changes.modified,
133
+ };
134
+ }
135
+ else {
136
+ result.contextTreeStatus = 'no_changes';
137
+ }
131
138
  }
132
139
  else {
133
- result.contextTreeStatus = 'no_changes';
140
+ result.contextTreeStatus = 'no_vc';
134
141
  }
135
142
  }
136
143
  }
@@ -41,10 +41,6 @@ export declare class VcHandler {
41
41
  setup(): void;
42
42
  private buildAuthorHint;
43
43
  private buildNoRemoteMessage;
44
- /**
45
- * Writes a .gitignore to the context-tree directory only if one does not already exist.
46
- */
47
- private ensureGitignore;
48
44
  /**
49
45
  * When force is NOT set, checks for uncommitted changes and throws
50
46
  * VcError(UNCOMMITTED_CHANGES) if the working tree is dirty.
@@ -1,13 +1,12 @@
1
1
  import fs from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { VcErrorCode, VcEvents, } from '../../../../shared/transport/events/vc-events.js';
4
- import { CONTEXT_TREE_GITIGNORE } from '../../../constants.js';
5
4
  import { BrvConfig } from '../../../core/domain/entities/brv-config.js';
6
5
  import { Space } from '../../../core/domain/entities/space.js';
7
6
  import { GitAuthError, GitError } from '../../../core/domain/errors/git-error.js';
8
7
  import { NotAuthenticatedError } from '../../../core/domain/errors/task-error.js';
9
8
  import { VcError } from '../../../core/domain/errors/vc-error.js';
10
- import { ensureGitignoreEntries } from '../../../utils/gitignore.js';
9
+ import { ensureContextTreeGitignore, ensureGitignoreEntries } from '../../../utils/gitignore.js';
11
10
  import { buildCogitRemoteUrl, isValidBranchName, parseUserFacingUrl } from '../../git/cogit-url.js';
12
11
  import { resolveRequiredProjectPath } from './handler-types.js';
13
12
  /**
@@ -101,18 +100,6 @@ export class VcHandler {
101
100
  ` 3. Run: brv vc remote add origin <url>\n` +
102
101
  ` 4. Then: ${nextStep}`);
103
102
  }
104
- /**
105
- * Writes a .gitignore to the context-tree directory only if one does not already exist.
106
- */
107
- async ensureGitignore(contextTreeDir) {
108
- const gitignorePath = join(contextTreeDir, '.gitignore');
109
- try {
110
- await fs.promises.access(gitignorePath);
111
- }
112
- catch {
113
- await fs.promises.writeFile(gitignorePath, CONTEXT_TREE_GITIGNORE, 'utf8');
114
- }
115
- }
116
103
  /**
117
104
  * When force is NOT set, checks for uncommitted changes and throws
118
105
  * VcError(UNCOMMITTED_CHANGES) if the working tree is dirty.
@@ -134,6 +121,7 @@ export class VcHandler {
134
121
  if (!gitInitialized) {
135
122
  throw new VcError('ByteRover version control not initialized.', VcErrorCode.GIT_NOT_INITIALIZED);
136
123
  }
124
+ await ensureContextTreeGitignore(directory);
137
125
  const statusBefore = await this.gitService.status({ directory });
138
126
  const stagedBefore = new Set(statusBefore.files.filter((f) => f.staged).map((f) => f.path));
139
127
  const hadUnstagedBefore = new Set(statusBefore.files.filter((f) => !f.staged).map((f) => f.path));
@@ -350,7 +338,7 @@ export class VcHandler {
350
338
  await this.projectConfigStore.write(updated, projectPath);
351
339
  }
352
340
  // Ensure .gitignore exists (remote may not have one)
353
- await this.ensureGitignore(contextTreeDir);
341
+ await ensureContextTreeGitignore(contextTreeDir);
354
342
  // Add .brv entries to project .gitignore (prevents `git add .` fatal error from nested .git)
355
343
  await ensureGitignoreEntries(projectPath);
356
344
  }
@@ -456,7 +444,7 @@ export class VcHandler {
456
444
  const reinitialized = await this.gitService.isInitialized({ directory: contextTreeDir });
457
445
  await this.gitService.init({ defaultBranch: 'main', directory: contextTreeDir });
458
446
  // 3. Ensure .gitignore exists with correct content (idempotent)
459
- await this.ensureGitignore(contextTreeDir);
447
+ await ensureContextTreeGitignore(contextTreeDir);
460
448
  // 4. Add .brv entries to project .gitignore (prevents `git add .` fatal error from nested .git)
461
449
  await ensureGitignoreEntries(projectPath);
462
450
  return {
@@ -818,6 +806,7 @@ export class VcHandler {
818
806
  untracked: [],
819
807
  };
820
808
  }
809
+ await ensureContextTreeGitignore(contextTreeDir);
821
810
  const branch = await this.gitService.getCurrentBranch({ directory: contextTreeDir });
822
811
  const gitStatus = await this.gitService.status({ directory: contextTreeDir });
823
812
  // Detect empty repo (no commits yet)