byterover-cli 1.2.0 → 1.3.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 (91) hide show
  1. package/README.md +87 -9
  2. package/dist/constants.d.ts +0 -5
  3. package/dist/constants.js +0 -5
  4. package/dist/core/domain/cipher/agent/agent-info.d.ts +17 -17
  5. package/dist/core/domain/cipher/llm/schemas.d.ts +14 -14
  6. package/dist/core/domain/cipher/session/session-metadata.d.ts +2 -2
  7. package/dist/core/domain/entities/agent.js +6 -6
  8. package/dist/core/domain/entities/connector-type.d.ts +2 -1
  9. package/dist/core/domain/entities/connector-type.js +2 -1
  10. package/dist/core/domain/transport/schemas.d.ts +66 -66
  11. package/dist/core/interfaces/cipher/i-chat-session.d.ts +3 -1
  12. package/dist/core/interfaces/connectors/i-connector.d.ts +2 -2
  13. package/dist/core/interfaces/i-file-service.d.ts +7 -0
  14. package/dist/infra/auth/oauth-service.d.ts +15 -0
  15. package/dist/infra/auth/oauth-service.js +38 -2
  16. package/dist/infra/cipher/agent/agent-schemas.d.ts +42 -42
  17. package/dist/infra/cipher/llm/context/context-manager.js +7 -9
  18. package/dist/infra/cipher/llm/internal-llm-service.d.ts +5 -1
  19. package/dist/infra/cipher/llm/internal-llm-service.js +57 -46
  20. package/dist/infra/cipher/session/chat-session.d.ts +3 -1
  21. package/dist/infra/cipher/session/chat-session.js +5 -3
  22. package/dist/infra/cipher/system-prompt/contributor-schemas.d.ts +8 -8
  23. package/dist/infra/cipher/system-prompt/schemas.d.ts +5 -5
  24. package/dist/infra/cipher/tools/implementations/task-tool.js +3 -3
  25. package/dist/infra/connectors/connector-manager.js +2 -0
  26. package/dist/infra/connectors/hook/hook-connector.d.ts +1 -1
  27. package/dist/infra/connectors/hook/hook-connector.js +3 -3
  28. package/dist/infra/connectors/mcp/mcp-connector.d.ts +1 -1
  29. package/dist/infra/connectors/mcp/mcp-connector.js +4 -4
  30. package/dist/infra/connectors/rules/rules-connector.d.ts +1 -1
  31. package/dist/infra/connectors/rules/rules-connector.js +4 -4
  32. package/dist/infra/connectors/shared/template-service.js +4 -0
  33. package/dist/infra/connectors/skill/index.d.ts +1 -0
  34. package/dist/infra/connectors/skill/index.js +1 -0
  35. package/dist/infra/connectors/skill/skill-connector-config.d.ts +45 -0
  36. package/dist/infra/connectors/skill/skill-connector-config.js +26 -0
  37. package/dist/infra/connectors/skill/skill-connector.d.ts +39 -0
  38. package/dist/infra/connectors/skill/skill-connector.js +160 -0
  39. package/dist/infra/connectors/skill/skill-content-loader.d.ts +18 -0
  40. package/dist/infra/connectors/skill/skill-content-loader.js +33 -0
  41. package/dist/infra/file/fs-file-service.d.ts +7 -0
  42. package/dist/infra/file/fs-file-service.js +15 -1
  43. package/dist/infra/mcp/tools/task-result-waiter.js +8 -0
  44. package/dist/infra/process/agent-worker.js +30 -14
  45. package/dist/infra/process/task-queue-manager.d.ts +23 -34
  46. package/dist/infra/process/task-queue-manager.js +57 -118
  47. package/dist/infra/process/transport-handlers.js +1 -7
  48. package/dist/infra/repl/commands/connectors-command.js +1 -1
  49. package/dist/infra/transport/socket-io-transport-client.d.ts +9 -0
  50. package/dist/infra/transport/socket-io-transport-client.js +21 -2
  51. package/dist/infra/usecase/connectors-use-case.js +8 -2
  52. package/dist/infra/usecase/init-use-case.js +1 -1
  53. package/dist/infra/usecase/reset-use-case.d.ts +1 -0
  54. package/dist/infra/usecase/reset-use-case.js +4 -1
  55. package/dist/{commands → oclif/commands}/curate.d.ts +1 -1
  56. package/dist/{commands → oclif/commands}/curate.js +6 -6
  57. package/dist/{commands → oclif/commands}/hook-prompt-submit.d.ts +1 -1
  58. package/dist/{commands → oclif/commands}/hook-prompt-submit.js +3 -3
  59. package/dist/{commands → oclif/commands}/main.js +10 -10
  60. package/dist/{commands → oclif/commands}/mcp.js +2 -2
  61. package/dist/{commands → oclif/commands}/query.d.ts +1 -1
  62. package/dist/{commands → oclif/commands}/query.js +6 -6
  63. package/dist/{commands → oclif/commands}/status.d.ts +1 -1
  64. package/dist/{commands → oclif/commands}/status.js +8 -8
  65. package/dist/{commands → oclif/commands}/watch.d.ts +3 -3
  66. package/dist/{commands → oclif/commands}/watch.js +6 -6
  67. package/dist/oclif/constants.d.ts +11 -0
  68. package/dist/oclif/constants.js +11 -0
  69. package/dist/{hooks → oclif/hooks}/prerun/validate-brv-config-version.d.ts +1 -1
  70. package/dist/{hooks → oclif/hooks}/prerun/validate-brv-config-version.js +2 -2
  71. package/dist/templates/sections/command-reference.md +5 -96
  72. package/dist/templates/sections/workflow.md +21 -16
  73. package/dist/templates/skill/SKILL.md +91 -0
  74. package/dist/templates/skill/TROUBLESHOOTING.md +50 -0
  75. package/dist/templates/skill/WORKFLOWS.md +229 -0
  76. package/dist/utils/type-guards.d.ts +11 -0
  77. package/dist/utils/type-guards.js +13 -0
  78. package/oclif.manifest.json +8 -1
  79. package/package.json +9 -9
  80. package/dist/infra/process/constants.d.ts +0 -1
  81. package/dist/infra/process/constants.js +0 -1
  82. /package/dist/{commands → oclif/commands}/main.d.ts +0 -0
  83. /package/dist/{commands → oclif/commands}/mcp.d.ts +0 -0
  84. /package/dist/{hooks → oclif/hooks}/command_not_found/handle-invalid-commands.d.ts +0 -0
  85. /package/dist/{hooks → oclif/hooks}/command_not_found/handle-invalid-commands.js +0 -0
  86. /package/dist/{hooks → oclif/hooks}/error/clean-errors.d.ts +0 -0
  87. /package/dist/{hooks → oclif/hooks}/error/clean-errors.js +0 -0
  88. /package/dist/{hooks → oclif/hooks}/init/update-notifier.d.ts +0 -0
  89. /package/dist/{hooks → oclif/hooks}/init/update-notifier.js +0 -0
  90. /package/dist/{hooks → oclif/hooks}/init/welcome.d.ts +0 -0
  91. /package/dist/{hooks → oclif/hooks}/init/welcome.js +0 -0
@@ -19,7 +19,7 @@ import { TomlMcpConfigWriter } from './toml-mcp-config-writer.js';
19
19
  * - Safe uninstall: Only removes ByteRover's MCP server entry and rule content
20
20
  */
21
21
  export class McpConnector {
22
- type = 'mcp';
22
+ connectorType = 'mcp';
23
23
  fileService;
24
24
  projectRoot;
25
25
  ruleFileManager;
@@ -34,7 +34,7 @@ export class McpConnector {
34
34
  projectRoot: options.projectRoot,
35
35
  });
36
36
  this.supportedAgents = Object.entries(AGENT_CONNECTOR_CONFIG)
37
- .filter(([_, config]) => config.supported.includes(this.type))
37
+ .filter(([_, config]) => config.supported.includes(this.connectorType))
38
38
  .map(([agent]) => agent);
39
39
  }
40
40
  getConfigPath(agent) {
@@ -66,7 +66,7 @@ export class McpConnector {
66
66
  return this.installAutomatic(agent, config);
67
67
  }
68
68
  isSupported(agent) {
69
- return agent in MCP_CONNECTOR_CONFIGS && AGENT_CONNECTOR_CONFIG[agent].supported.includes(this.type);
69
+ return agent in MCP_CONNECTOR_CONFIGS && AGENT_CONNECTOR_CONFIG[agent].supported.includes(this.connectorType);
70
70
  }
71
71
  async status(agent) {
72
72
  if (!this.isSupported(agent)) {
@@ -254,7 +254,7 @@ export class McpConnector {
254
254
  if (!rulesConfig) {
255
255
  return;
256
256
  }
257
- const ruleContent = await this.templateService.generateRuleContent(agent, this.type);
257
+ const ruleContent = await this.templateService.generateRuleContent(agent, this.connectorType);
258
258
  await this.ruleFileManager.install(rulesConfig.filePath, rulesConfig.writeMode, ruleContent);
259
259
  }
260
260
  /**
@@ -17,7 +17,7 @@ type RulesConnectorOptions = {
17
17
  * Manages the installation, uninstallation, and status of rule files.
18
18
  */
19
19
  export declare class RulesConnector implements IConnector {
20
- readonly type: ConnectorType;
20
+ readonly connectorType: ConnectorType;
21
21
  private readonly fileService;
22
22
  private readonly projectRoot;
23
23
  private readonly ruleFileManager;
@@ -7,7 +7,7 @@ import { RULES_CONNECTOR_CONFIGS } from './rules-connector-config.js';
7
7
  * Manages the installation, uninstallation, and status of rule files.
8
8
  */
9
9
  export class RulesConnector {
10
- type = 'rules';
10
+ connectorType = 'rules';
11
11
  fileService;
12
12
  projectRoot;
13
13
  ruleFileManager;
@@ -22,7 +22,7 @@ export class RulesConnector {
22
22
  projectRoot: options.projectRoot,
23
23
  });
24
24
  this.supportedAgents = Object.entries(AGENT_CONNECTOR_CONFIG)
25
- .filter(([_, config]) => config.supported.includes(this.type))
25
+ .filter(([_, config]) => config.supported.includes(this.connectorType))
26
26
  .map(([agent]) => agent);
27
27
  }
28
28
  getConfigPath(agent) {
@@ -34,7 +34,7 @@ export class RulesConnector {
34
34
  async install(agent) {
35
35
  const config = RULES_CONNECTOR_CONFIGS[agent];
36
36
  try {
37
- const ruleContent = await this.templateService.generateRuleContent(agent, this.type);
37
+ const ruleContent = await this.templateService.generateRuleContent(agent, this.connectorType);
38
38
  // Write the rule content to the file
39
39
  await this.ruleFileManager.install(config.filePath, config.writeMode, ruleContent);
40
40
  return {
@@ -54,7 +54,7 @@ export class RulesConnector {
54
54
  }
55
55
  }
56
56
  isSupported(agent) {
57
- return AGENT_CONNECTOR_CONFIG[agent].supported.includes(this.type);
57
+ return AGENT_CONNECTOR_CONFIG[agent].supported.includes(this.connectorType);
58
58
  }
59
59
  async status(agent) {
60
60
  const config = RULES_CONNECTOR_CONFIGS[agent];
@@ -8,6 +8,10 @@ import { BRV_RULE_MARKERS, BRV_RULE_TAG } from './constants.js';
8
8
  * @returns The wrapped content with boundary markers.
9
9
  */
10
10
  const wrapContentWithBoundaryMarkers = (content, agent, header) => {
11
+ if (agent === 'Windsurf' && header !== '') {
12
+ const parts = [header, BRV_RULE_MARKERS.START, content, '---', `${BRV_RULE_TAG} ${agent}`, BRV_RULE_MARKERS.END];
13
+ return parts.join('\n');
14
+ }
11
15
  const parts = [BRV_RULE_MARKERS.START, header, content, '---', `${BRV_RULE_TAG} ${agent}`, BRV_RULE_MARKERS.END];
12
16
  return parts.join('\n');
13
17
  };
@@ -0,0 +1 @@
1
+ export { SkillConnector } from './skill-connector.js';
@@ -0,0 +1 @@
1
+ export { SkillConnector } from './skill-connector.js';
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Scope for skill connector configuration.
3
+ * - 'project': Path relative to project root
4
+ * - 'global': Path relative to user home directory
5
+ */
6
+ export type SkillConfigScope = 'global' | 'project';
7
+ /**
8
+ * Configuration for agent-specific skill file directories.
9
+ */
10
+ export type SkillConnectorConfig = {
11
+ /** Base directory for skill files (relative to scope root) */
12
+ basePath: string;
13
+ /** Whether path is relative to project root or home dir */
14
+ scope: SkillConfigScope;
15
+ };
16
+ /**
17
+ * Agent-specific skill connector configurations.
18
+ * Maps each supported agent to its skill directory path and scope.
19
+ */
20
+ export declare const SKILL_CONNECTOR_CONFIGS: {
21
+ readonly 'Claude Code': {
22
+ readonly basePath: ".claude/skills/byterover";
23
+ readonly scope: "project";
24
+ };
25
+ readonly Codex: {
26
+ readonly basePath: ".codex/skills/byterover";
27
+ readonly scope: "global";
28
+ };
29
+ readonly Cursor: {
30
+ readonly basePath: ".cursor/skills/byterover";
31
+ readonly scope: "project";
32
+ };
33
+ readonly 'Github Copilot': {
34
+ readonly basePath: ".github/skills/byterover";
35
+ readonly scope: "project";
36
+ };
37
+ };
38
+ /**
39
+ * Type representing agents that have skill connector support.
40
+ */
41
+ export type SkillSupportedAgent = keyof typeof SKILL_CONNECTOR_CONFIGS;
42
+ /**
43
+ * Names of the skill files written by the skill connector.
44
+ */
45
+ export declare const SKILL_FILE_NAMES: readonly ["SKILL.md", "TROUBLESHOOTING.md", "WORKFLOWS.md"];
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Agent-specific skill connector configurations.
3
+ * Maps each supported agent to its skill directory path and scope.
4
+ */
5
+ export const SKILL_CONNECTOR_CONFIGS = {
6
+ 'Claude Code': {
7
+ basePath: '.claude/skills/byterover',
8
+ scope: 'project',
9
+ },
10
+ Codex: {
11
+ basePath: '.codex/skills/byterover',
12
+ scope: 'global',
13
+ },
14
+ Cursor: {
15
+ basePath: '.cursor/skills/byterover',
16
+ scope: 'project',
17
+ },
18
+ 'Github Copilot': {
19
+ basePath: '.github/skills/byterover',
20
+ scope: 'project',
21
+ },
22
+ };
23
+ /**
24
+ * Names of the skill files written by the skill connector.
25
+ */
26
+ export const SKILL_FILE_NAMES = ['SKILL.md', 'TROUBLESHOOTING.md', 'WORKFLOWS.md'];
@@ -0,0 +1,39 @@
1
+ import type { Agent } from '../../../core/domain/entities/agent.js';
2
+ import type { ConnectorType } from '../../../core/domain/entities/connector-type.js';
3
+ import type { ConnectorInstallResult, ConnectorStatus, ConnectorUninstallResult } from '../../../core/interfaces/connectors/connector-types.js';
4
+ import type { IConnector } from '../../../core/interfaces/connectors/i-connector.js';
5
+ import type { IFileService } from '../../../core/interfaces/i-file-service.js';
6
+ import type { SkillSupportedAgent } from './skill-connector-config.js';
7
+ /**
8
+ * Options for constructing SkillConnector.
9
+ */
10
+ type SkillConnectorOptions = {
11
+ fileService: IFileService;
12
+ projectRoot: string;
13
+ };
14
+ /**
15
+ * Connector that integrates BRV with coding agents via skill files.
16
+ * Writes static markdown files (SKILL.md, TROUBLESHOOTING.md, WORKFLOWS.md)
17
+ * into an agent-specific subdirectory.
18
+ */
19
+ export declare class SkillConnector implements IConnector {
20
+ readonly connectorType: ConnectorType;
21
+ private readonly contentLoader;
22
+ private readonly fileService;
23
+ private readonly projectRoot;
24
+ private readonly supportedAgents;
25
+ constructor(options: SkillConnectorOptions);
26
+ getConfigPath(agent: Agent): string;
27
+ getSupportedAgents(): Agent[];
28
+ install(agent: Agent): Promise<ConnectorInstallResult>;
29
+ isSupported(agent: Agent): agent is SkillSupportedAgent;
30
+ status(agent: Agent): Promise<ConnectorStatus>;
31
+ uninstall(agent: Agent): Promise<ConnectorUninstallResult>;
32
+ /**
33
+ * Get the full (absolute) path for skill file operations.
34
+ * - Project scope: relative to project root
35
+ * - Global scope: relative to os.homedir()
36
+ */
37
+ private getFullPath;
38
+ }
39
+ export {};
@@ -0,0 +1,160 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import { AGENT_CONNECTOR_CONFIG } from '../../../core/domain/entities/agent.js';
4
+ import { SKILL_CONNECTOR_CONFIGS, SKILL_FILE_NAMES } from './skill-connector-config.js';
5
+ import { SkillContentLoader } from './skill-content-loader.js';
6
+ /**
7
+ * Connector that integrates BRV with coding agents via skill files.
8
+ * Writes static markdown files (SKILL.md, TROUBLESHOOTING.md, WORKFLOWS.md)
9
+ * into an agent-specific subdirectory.
10
+ */
11
+ export class SkillConnector {
12
+ connectorType = 'skill';
13
+ contentLoader;
14
+ fileService;
15
+ projectRoot;
16
+ supportedAgents;
17
+ constructor(options) {
18
+ this.fileService = options.fileService;
19
+ this.projectRoot = options.projectRoot;
20
+ this.contentLoader = new SkillContentLoader(options.fileService);
21
+ this.supportedAgents = Object.entries(AGENT_CONNECTOR_CONFIG)
22
+ .filter(([_, config]) => config.supported.includes(this.connectorType))
23
+ .map(([agent]) => agent);
24
+ }
25
+ getConfigPath(agent) {
26
+ if (!this.isSupported(agent)) {
27
+ throw new Error(`Skill connector does not support agent: ${agent}`);
28
+ }
29
+ return SKILL_CONNECTOR_CONFIGS[agent].basePath;
30
+ }
31
+ getSupportedAgents() {
32
+ return this.supportedAgents;
33
+ }
34
+ async install(agent) {
35
+ if (!this.isSupported(agent)) {
36
+ return {
37
+ alreadyInstalled: false,
38
+ configPath: '',
39
+ message: `Skill connector does not support agent: ${agent}`,
40
+ success: false,
41
+ };
42
+ }
43
+ const config = SKILL_CONNECTOR_CONFIGS[agent];
44
+ const fullDir = this.getFullPath(config.basePath, config.scope);
45
+ try {
46
+ // Check if already installed
47
+ const skillFilePath = path.join(fullDir, SKILL_FILE_NAMES[0]);
48
+ if (await this.fileService.exists(skillFilePath)) {
49
+ return {
50
+ alreadyInstalled: true,
51
+ configPath: config.basePath,
52
+ message: `Skill connector is already installed for ${agent}`,
53
+ success: true,
54
+ };
55
+ }
56
+ // Write all skill files
57
+ await Promise.all(SKILL_FILE_NAMES.map(async (fileName) => {
58
+ const content = await this.contentLoader.loadSkillFile(fileName);
59
+ const filePath = path.join(fullDir, fileName);
60
+ await this.fileService.write(content, filePath, 'overwrite');
61
+ }));
62
+ return {
63
+ alreadyInstalled: false,
64
+ configPath: fullDir,
65
+ message: `Skill connector installed for ${agent} (created ${fullDir}/)`,
66
+ success: true,
67
+ };
68
+ }
69
+ catch (error) {
70
+ return {
71
+ alreadyInstalled: false,
72
+ configPath: config.basePath,
73
+ message: `Failed to install skill connector for ${agent}: ${error instanceof Error ? error.message : String(error)}`,
74
+ success: false,
75
+ };
76
+ }
77
+ }
78
+ isSupported(agent) {
79
+ return agent in SKILL_CONNECTOR_CONFIGS && AGENT_CONNECTOR_CONFIG[agent].supported.includes(this.connectorType);
80
+ }
81
+ async status(agent) {
82
+ if (!this.isSupported(agent)) {
83
+ return {
84
+ configExists: false,
85
+ configPath: '',
86
+ error: `Skill connector does not support agent: ${agent}`,
87
+ installed: false,
88
+ };
89
+ }
90
+ const config = SKILL_CONNECTOR_CONFIGS[agent];
91
+ const fullDir = this.getFullPath(config.basePath, config.scope);
92
+ try {
93
+ const skillFilePath = path.join(fullDir, SKILL_FILE_NAMES[0]);
94
+ const exists = await this.fileService.exists(skillFilePath);
95
+ return {
96
+ configExists: exists,
97
+ configPath: config.basePath,
98
+ installed: exists,
99
+ };
100
+ }
101
+ catch (error) {
102
+ return {
103
+ configExists: false,
104
+ configPath: config.basePath,
105
+ error: error instanceof Error ? error.message : String(error),
106
+ installed: false,
107
+ };
108
+ }
109
+ }
110
+ async uninstall(agent) {
111
+ if (!this.isSupported(agent)) {
112
+ return {
113
+ configPath: '',
114
+ message: `Skill connector does not support agent: ${agent}`,
115
+ success: false,
116
+ wasInstalled: false,
117
+ };
118
+ }
119
+ const config = SKILL_CONNECTOR_CONFIGS[agent];
120
+ const fullDir = this.getFullPath(config.basePath, config.scope);
121
+ try {
122
+ const skillFilePath = path.join(fullDir, SKILL_FILE_NAMES[0]);
123
+ const exists = await this.fileService.exists(skillFilePath);
124
+ if (!exists) {
125
+ return {
126
+ configPath: config.basePath,
127
+ message: `Skill connector is not installed for ${agent}`,
128
+ success: true,
129
+ wasInstalled: false,
130
+ };
131
+ }
132
+ await this.fileService.deleteDirectory(fullDir);
133
+ return {
134
+ configPath: config.basePath,
135
+ message: `Skill connector uninstalled for ${agent}`,
136
+ success: true,
137
+ wasInstalled: true,
138
+ };
139
+ }
140
+ catch (error) {
141
+ return {
142
+ configPath: config.basePath,
143
+ message: `Failed to uninstall skill connector for ${agent}: ${error instanceof Error ? error.message : String(error)}`,
144
+ success: false,
145
+ wasInstalled: true,
146
+ };
147
+ }
148
+ }
149
+ /**
150
+ * Get the full (absolute) path for skill file operations.
151
+ * - Project scope: relative to project root
152
+ * - Global scope: relative to os.homedir()
153
+ */
154
+ getFullPath(basePath, scope) {
155
+ if (scope === 'global') {
156
+ return path.join(os.homedir(), basePath);
157
+ }
158
+ return path.join(this.projectRoot, basePath);
159
+ }
160
+ }
@@ -0,0 +1,18 @@
1
+ import type { IFileService } from '../../../core/interfaces/i-file-service.js';
2
+ /**
3
+ * Loads static skill markdown files from the templates/skill/ directory.
4
+ * Uses the same import.meta.url path resolution pattern as FsTemplateLoader.
5
+ */
6
+ export declare class SkillContentLoader {
7
+ private readonly fileService;
8
+ private readonly skillDir;
9
+ constructor(fileService: IFileService);
10
+ /**
11
+ * Loads a skill file by name from the templates/skill/ directory.
12
+ *
13
+ * @param fileName - Name of the skill file (e.g., 'SKILL.md')
14
+ * @returns Promise resolving to the file content
15
+ * @throws Error if the file cannot be read
16
+ */
17
+ loadSkillFile(fileName: string): Promise<string>;
18
+ }
@@ -0,0 +1,33 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ /**
4
+ * Loads static skill markdown files from the templates/skill/ directory.
5
+ * Uses the same import.meta.url path resolution pattern as FsTemplateLoader.
6
+ */
7
+ export class SkillContentLoader {
8
+ fileService;
9
+ skillDir;
10
+ constructor(fileService) {
11
+ this.fileService = fileService;
12
+ const currentFilePath = fileURLToPath(import.meta.url);
13
+ const currentDir = path.dirname(currentFilePath);
14
+ // Navigate from src/infra/connectors/skill/ to src/templates/skill/
15
+ this.skillDir = path.join(currentDir, '..', '..', '..', 'templates', 'skill');
16
+ }
17
+ /**
18
+ * Loads a skill file by name from the templates/skill/ directory.
19
+ *
20
+ * @param fileName - Name of the skill file (e.g., 'SKILL.md')
21
+ * @returns Promise resolving to the file content
22
+ * @throws Error if the file cannot be read
23
+ */
24
+ async loadSkillFile(fileName) {
25
+ const fullPath = path.join(this.skillDir, fileName);
26
+ try {
27
+ return await this.fileService.read(fullPath);
28
+ }
29
+ catch (error) {
30
+ throw new Error(`Failed to load skill file '${fileName}': ${error instanceof Error ? error.message : String(error)}`);
31
+ }
32
+ }
33
+ }
@@ -11,6 +11,13 @@ export declare class FsFileService implements IFileService {
11
11
  * @returns A promise that resolves when the file has been deleted.
12
12
  */
13
13
  delete(filePath: string): Promise<void>;
14
+ /**
15
+ * Deletes a directory and all its contents recursively.
16
+ *
17
+ * @param dirPath The path to the directory to delete.
18
+ * @returns A promise that resolves when the directory has been deleted.
19
+ */
20
+ deleteDirectory(dirPath: string): Promise<void>;
14
21
  /**
15
22
  * Checks if a file exists at the specified path.
16
23
  *
@@ -1,4 +1,4 @@
1
- import { access, appendFile, copyFile, mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
1
+ import { access, appendFile, copyFile, mkdir, readFile, rm, unlink, writeFile } from 'node:fs/promises';
2
2
  import { dirname } from 'node:path';
3
3
  /**
4
4
  * File service implementation using Node.js fs module.
@@ -30,6 +30,20 @@ export class FsFileService {
30
30
  throw new Error(`Failed to delete file '${filePath}': ${error instanceof Error ? error.message : 'Unknown error'}`);
31
31
  }
32
32
  }
33
+ /**
34
+ * Deletes a directory and all its contents recursively.
35
+ *
36
+ * @param dirPath The path to the directory to delete.
37
+ * @returns A promise that resolves when the directory has been deleted.
38
+ */
39
+ async deleteDirectory(dirPath) {
40
+ try {
41
+ await rm(dirPath, { force: true, recursive: true });
42
+ }
43
+ catch (error) {
44
+ throw new Error(`Failed to delete directory '${dirPath}': ${error instanceof Error ? error.message : 'Unknown error'}`);
45
+ }
46
+ }
33
47
  /**
34
48
  * Checks if a file exists at the specified path.
35
49
  *
@@ -29,6 +29,14 @@ export async function waitForTaskResult(client, taskId, timeoutMs = 120_000) {
29
29
  }, timeoutMs);
30
30
  // Set up all event listeners
31
31
  unsubscribers.push(
32
+ // Listen for connection state changes - fail fast on disconnect
33
+ client.onStateChange((state) => {
34
+ if (state === 'disconnected' && !completed) {
35
+ completed = true;
36
+ cleanup();
37
+ reject(new Error('Connection lost to ByteRover instance'));
38
+ }
39
+ }),
32
40
  // Listen for LLM response content
33
41
  client.on('llmservice:response', (payload) => {
34
42
  if (payload.taskId === taskId && payload.content) {
@@ -29,7 +29,6 @@ import { QueryExecutor } from '../core/executors/query-executor.js';
29
29
  import { createTaskProcessor } from '../core/task-processor.js';
30
30
  import { createTokenStore } from '../storage/token-store.js';
31
31
  import { createTransportClient } from '../transport/transport-factory.js';
32
- import { CURATE_MAX_CONCURRENT } from './constants.js';
33
32
  import { createParentHeartbeat } from './parent-heartbeat.js';
34
33
  import { TaskQueueManager } from './task-queue-manager.js';
35
34
  // IPC types imported from ./ipc-types.ts
@@ -104,19 +103,37 @@ let eventForwarders = [];
104
103
  // ============================================================================
105
104
  /**
106
105
  * Task queue manager handles:
107
- * - Separate queues for curate and query tasks
108
- * - Concurrency limits (max 1 concurrent per type)
106
+ * - Unified queue for all task types (curate, query)
107
+ * - Sequential FIFO execution (max 1 concurrent)
109
108
  * - Task deduplication (same taskId can't be queued twice)
110
109
  * - Cancel tasks from queue before processing
111
- * - FIFO processing order
112
110
  */
113
111
  const taskQueueManager = new TaskQueueManager({
114
- curate: { maxConcurrent: CURATE_MAX_CONCURRENT },
112
+ maxConcurrent: 1, // Sequential FIFO execution for all tasks
115
113
  onExecutorError(taskId, error) {
116
114
  agentLog(`Executor error for task ${taskId}: ${error}`);
117
115
  },
118
- query: { maxConcurrent: Infinity },
119
116
  });
117
+ /**
118
+ * Notify clients about dropped tasks and clear the queue.
119
+ * Extracted to avoid DRY violation across reinit, stop, and shutdown paths.
120
+ */
121
+ function notifyQueuedTasksAboutDropAndClear(reason) {
122
+ const queuedTasks = taskQueueManager.getQueuedTasks();
123
+ if (queuedTasks.length > 0) {
124
+ const error = serializeTaskError(new AgentNotInitializedError(`Task dropped - ${reason}`));
125
+ if (transportClient) {
126
+ agentLog(`Notifying ${queuedTasks.length} queued task(s): ${reason}`);
127
+ for (const task of queuedTasks) {
128
+ transportClient.request('task:error', { error, taskId: task.taskId }).catch(logTransportError);
129
+ }
130
+ }
131
+ else {
132
+ agentLog(`Cannot notify ${queuedTasks.length} queued task(s): no transport client`);
133
+ }
134
+ }
135
+ taskQueueManager.clear();
136
+ }
120
137
  /**
121
138
  * Get Transport port from environment.
122
139
  */
@@ -318,7 +335,7 @@ const TASK_EXECUTION_TIMEOUT_MS = 5 * 60 * 1000;
318
335
  function setupTaskExecutor() {
319
336
  taskQueueManager.setExecutor(async (task) => {
320
337
  const { taskId, type } = task;
321
- const stats = taskQueueManager.getStats(type);
338
+ const stats = taskQueueManager.getStats();
322
339
  agentLog(`Processing task ${taskId} (${type}), ${stats.queued} queued, ${stats.active} active`);
323
340
  // Fix #2: Lazy initialization - if agent not ready, try to initialize now
324
341
  // This enables processing tasks that arrived while init was failing
@@ -499,9 +516,8 @@ async function tryInitializeAgent(forceReinit = false) {
499
516
  try {
500
517
  // If forcing reinit, drain queue and stop existing agent first
501
518
  if (forceReinit) {
502
- // Drain task queue before reinit to prevent tasks executing with stale processor
503
519
  agentLog('Draining task queue before reinit...');
504
- taskQueueManager.clear(); // Clear queued (not yet started) tasks
520
+ notifyQueuedTasksAboutDropAndClear('credential/config change');
505
521
  // Wait for active tasks to complete (with timeout)
506
522
  await waitForActiveTasksToComplete(10_000);
507
523
  await stopExistingAgentForReinit();
@@ -727,7 +743,7 @@ async function startAgent() {
727
743
  }
728
744
  const result = taskQueueManager.enqueue(data);
729
745
  if (result.success) {
730
- const stats = taskQueueManager.getStats(data.type);
746
+ const stats = taskQueueManager.getStats();
731
747
  agentLog(`Task ${data.taskId} (${data.type}) queued at position ${result.position}, ${stats.queued} in queue`);
732
748
  }
733
749
  else if (result.reason === 'duplicate') {
@@ -857,8 +873,8 @@ async function startAgent() {
857
873
  async function stopCipherAgent() {
858
874
  // Cleanup event forwarders
859
875
  cleanupAgentEventForwarding();
860
- // Clear task queue (can't process without agent)
861
- taskQueueManager.clear();
876
+ // Notify and clear task queue (can't process without agent)
877
+ notifyQueuedTasksAboutDropAndClear('agent stopped');
862
878
  // Stop CipherAgent
863
879
  if (cipherAgent) {
864
880
  try {
@@ -1098,8 +1114,8 @@ async function stopAgent() {
1098
1114
  // Stop polling and heartbeat first
1099
1115
  stopCredentialsPolling();
1100
1116
  parentHeartbeat?.stop();
1101
- // Clear task queue
1102
- taskQueueManager.clear();
1117
+ // Notify and clear task queue
1118
+ notifyQueuedTasksAboutDropAndClear('agent shutting down');
1103
1119
  // Cleanup event forwarders before stopping agent
1104
1120
  cleanupAgentEventForwarding();
1105
1121
  // Stop CipherAgent first