icopilot 2.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 (203) hide show
  1. package/CHANGELOG.md +250 -0
  2. package/LICENSE +21 -0
  3. package/README.md +214 -0
  4. package/bin/icopilot.js +6 -0
  5. package/dist/acp/router.js +123 -0
  6. package/dist/acp/schema.js +53 -0
  7. package/dist/agents/aggregator.js +187 -0
  8. package/dist/agents/custom-agents.js +97 -0
  9. package/dist/agents/goal-driven.js +411 -0
  10. package/dist/agents/multi-repo.js +350 -0
  11. package/dist/agents/parallel-runner.js +181 -0
  12. package/dist/agents/router.js +144 -0
  13. package/dist/agents/self-heal.js +481 -0
  14. package/dist/agents/tdd-agent.js +278 -0
  15. package/dist/api/github-models.js +158 -0
  16. package/dist/bridge/ide-bridge.js +479 -0
  17. package/dist/cloud/routine-executor.js +34 -0
  18. package/dist/cloud/routine-scheduler.js +67 -0
  19. package/dist/cloud/routine-storage.js +297 -0
  20. package/dist/commands/acp-cmd.js +143 -0
  21. package/dist/commands/actions-cmd.js +624 -0
  22. package/dist/commands/agent-cmd.js +144 -0
  23. package/dist/commands/alias-cmd.js +132 -0
  24. package/dist/commands/bookmark-cmd.js +77 -0
  25. package/dist/commands/changelog-cmd.js +99 -0
  26. package/dist/commands/changes-cmd.js +120 -0
  27. package/dist/commands/clipboard-cmd.js +217 -0
  28. package/dist/commands/cloud-routine-cmd.js +265 -0
  29. package/dist/commands/codegen-cmd.js +544 -0
  30. package/dist/commands/compare-cmd.js +116 -0
  31. package/dist/commands/context-cmd.js +247 -0
  32. package/dist/commands/context-viz-cmd.js +43 -0
  33. package/dist/commands/conventions-cmd.js +116 -0
  34. package/dist/commands/cost-cmd.js +51 -0
  35. package/dist/commands/deps-cmd.js +294 -0
  36. package/dist/commands/diagram-cmd.js +658 -0
  37. package/dist/commands/diff-review-cmd.js +92 -0
  38. package/dist/commands/doc-cmd.js +412 -0
  39. package/dist/commands/doctor-cmd.js +152 -0
  40. package/dist/commands/editor-cmd.js +49 -0
  41. package/dist/commands/env-cmd.js +86 -0
  42. package/dist/commands/explain-cmd.js +78 -0
  43. package/dist/commands/explain-shell-cmd.js +22 -0
  44. package/dist/commands/explore-cmd.js +231 -0
  45. package/dist/commands/feedback-cmd.js +98 -0
  46. package/dist/commands/fix-cmd.js +17 -0
  47. package/dist/commands/generate-cmd.js +38 -0
  48. package/dist/commands/git-extra.js +197 -0
  49. package/dist/commands/git-log-cmd.js +98 -0
  50. package/dist/commands/git-undo-cmd.js +137 -0
  51. package/dist/commands/git.js +155 -0
  52. package/dist/commands/history-cmd.js +122 -0
  53. package/dist/commands/index-cmd.js +65 -0
  54. package/dist/commands/init-cmd.js +73 -0
  55. package/dist/commands/lint-cmd.js +133 -0
  56. package/dist/commands/memory-cmd.js +98 -0
  57. package/dist/commands/metrics-cmd.js +97 -0
  58. package/dist/commands/mode-prefix.js +30 -0
  59. package/dist/commands/multi-cmd.js +44 -0
  60. package/dist/commands/notify-cmd.js +204 -0
  61. package/dist/commands/profile-cmd.js +101 -0
  62. package/dist/commands/prompts.js +17 -0
  63. package/dist/commands/rag-cmd.js +60 -0
  64. package/dist/commands/readme-cmd.js +564 -0
  65. package/dist/commands/reasoning-cmd.js +34 -0
  66. package/dist/commands/refactor-cmd.js +96 -0
  67. package/dist/commands/release-cmd.js +450 -0
  68. package/dist/commands/repo-cmd.js +195 -0
  69. package/dist/commands/route-cmd.js +21 -0
  70. package/dist/commands/schedule-cmd.js +109 -0
  71. package/dist/commands/search-cmd.js +47 -0
  72. package/dist/commands/security-cmd.js +156 -0
  73. package/dist/commands/settings-cmd.js +238 -0
  74. package/dist/commands/skill-cmd.js +338 -0
  75. package/dist/commands/slash.js +2721 -0
  76. package/dist/commands/snippets-cmd.js +83 -0
  77. package/dist/commands/space-cmd.js +92 -0
  78. package/dist/commands/stash-cmd.js +156 -0
  79. package/dist/commands/stats-cmd.js +36 -0
  80. package/dist/commands/style-cmd.js +85 -0
  81. package/dist/commands/suggest-cmd.js +40 -0
  82. package/dist/commands/summary-cmd.js +138 -0
  83. package/dist/commands/task-cmd.js +58 -0
  84. package/dist/commands/team-memory-cmd.js +97 -0
  85. package/dist/commands/template-cmd.js +475 -0
  86. package/dist/commands/test-cmd.js +146 -0
  87. package/dist/commands/todo-cmd.js +172 -0
  88. package/dist/commands/tokens-cmd.js +277 -0
  89. package/dist/commands/trigger-cmd.js +147 -0
  90. package/dist/commands/undo-cmd.js +18 -0
  91. package/dist/commands/voice-cmd.js +89 -0
  92. package/dist/commands/watch-cmd.js +110 -0
  93. package/dist/commands/web-cmd.js +183 -0
  94. package/dist/commands/worktree-cmd.js +119 -0
  95. package/dist/config-profile.js +66 -0
  96. package/dist/config.js +288 -0
  97. package/dist/context/compactor.js +53 -0
  98. package/dist/context/dep-context.js +329 -0
  99. package/dist/context/file-refs.js +54 -0
  100. package/dist/context/git-context.js +229 -0
  101. package/dist/context/image-input.js +66 -0
  102. package/dist/context/memory.js +55 -0
  103. package/dist/context/persistent-memory.js +104 -0
  104. package/dist/context/pinned.js +96 -0
  105. package/dist/context/priority.js +150 -0
  106. package/dist/context/read-only.js +48 -0
  107. package/dist/context/smart-files.js +286 -0
  108. package/dist/context/team-memory.js +156 -0
  109. package/dist/extensions/loader.js +149 -0
  110. package/dist/extensions/marketplace.js +49 -0
  111. package/dist/extensions/slack-provider.js +181 -0
  112. package/dist/extensions/team.js +56 -0
  113. package/dist/extensions/teams-provider.js +222 -0
  114. package/dist/extensions/voice.js +18 -0
  115. package/dist/hooks/lifecycle.js +215 -0
  116. package/dist/hooks/precommit.js +463 -0
  117. package/dist/index/embeddings.js +23 -0
  118. package/dist/index/indexer.js +86 -0
  119. package/dist/index/retrieve.js +20 -0
  120. package/dist/index/store.js +95 -0
  121. package/dist/index.js +286 -0
  122. package/dist/intelligence/dead-code.js +457 -0
  123. package/dist/intelligence/error-watch.js +263 -0
  124. package/dist/intelligence/navigation.js +141 -0
  125. package/dist/intelligence/stack-trace.js +210 -0
  126. package/dist/intelligence/symbol-index.js +410 -0
  127. package/dist/knowledge/auto-memory.js +412 -0
  128. package/dist/knowledge/conventions.js +475 -0
  129. package/dist/knowledge/corrections.js +213 -0
  130. package/dist/knowledge/rag.js +450 -0
  131. package/dist/knowledge/style-learner.js +324 -0
  132. package/dist/logger.js +35 -0
  133. package/dist/mcp/client.js +144 -0
  134. package/dist/mcp/config.js +24 -0
  135. package/dist/mcp/index.js +89 -0
  136. package/dist/modes/auto-compact.js +20 -0
  137. package/dist/modes/autopilot.js +157 -0
  138. package/dist/modes/background.js +82 -0
  139. package/dist/modes/interactive.js +187 -0
  140. package/dist/modes/oneshot.js +36 -0
  141. package/dist/modes/tui.js +265 -0
  142. package/dist/modes/turn.js +342 -0
  143. package/dist/notifications/manager.js +107 -0
  144. package/dist/plugins/marketplace.js +244 -0
  145. package/dist/providers/custom-provider.js +298 -0
  146. package/dist/providers/local-model.js +121 -0
  147. package/dist/routing/profiles.js +44 -0
  148. package/dist/routing/router.js +18 -0
  149. package/dist/sandbox/container.js +151 -0
  150. package/dist/security/audit.js +237 -0
  151. package/dist/security/content-filter.js +449 -0
  152. package/dist/security/proxy.js +301 -0
  153. package/dist/security/retention.js +281 -0
  154. package/dist/security/roles.js +252 -0
  155. package/dist/server/api-server.js +679 -0
  156. package/dist/session/bookmarks.js +72 -0
  157. package/dist/session/cloud-session.js +291 -0
  158. package/dist/session/handoff.js +405 -0
  159. package/dist/session/manager.js +35 -0
  160. package/dist/session/session.js +296 -0
  161. package/dist/session/share.js +313 -0
  162. package/dist/session/undo-journal.js +91 -0
  163. package/dist/snippets/store.js +60 -0
  164. package/dist/spaces/space-config.js +156 -0
  165. package/dist/spaces/space.js +220 -0
  166. package/dist/stats/store.js +101 -0
  167. package/dist/tools/apply-patch.js +134 -0
  168. package/dist/tools/auto-check.js +218 -0
  169. package/dist/tools/diff-edit.js +150 -0
  170. package/dist/tools/diff-prompt.js +36 -0
  171. package/dist/tools/edit-file.js +66 -0
  172. package/dist/tools/file-ops.js +205 -0
  173. package/dist/tools/glob.js +17 -0
  174. package/dist/tools/grep.js +56 -0
  175. package/dist/tools/image.js +194 -0
  176. package/dist/tools/list-directory.js +228 -0
  177. package/dist/tools/memory.js +17 -0
  178. package/dist/tools/multi-edit.js +299 -0
  179. package/dist/tools/policy.js +95 -0
  180. package/dist/tools/registry.js +484 -0
  181. package/dist/tools/retry.js +74 -0
  182. package/dist/tools/run-in-terminal.js +162 -0
  183. package/dist/tools/safety.js +64 -0
  184. package/dist/tools/sandbox.js +15 -0
  185. package/dist/tools/search-symbols.js +212 -0
  186. package/dist/tools/shell.js +118 -0
  187. package/dist/tools/web.js +167 -0
  188. package/dist/ui/prompt.js +37 -0
  189. package/dist/ui/render.js +96 -0
  190. package/dist/ui/screen.js +13 -0
  191. package/dist/ui/theme.js +56 -0
  192. package/dist/util/browser.js +34 -0
  193. package/dist/util/completion.js +350 -0
  194. package/dist/util/cost.js +28 -0
  195. package/dist/util/keybindings.js +113 -0
  196. package/dist/util/lazy.js +26 -0
  197. package/dist/util/perf.js +25 -0
  198. package/dist/util/token-worker.js +11 -0
  199. package/dist/util/tokens.js +50 -0
  200. package/dist/workflows/builtins.js +128 -0
  201. package/dist/workflows/engine.js +496 -0
  202. package/dist/workflows/file-trigger.js +197 -0
  203. package/package.json +79 -0
@@ -0,0 +1,350 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import fg from 'fast-glob';
4
+ import simpleGit from 'simple-git';
5
+ import { parseDocument, stringify } from 'yaml';
6
+ const CONFIG_DIR = '.icopilot';
7
+ const CONFIG_FILE = 'repos.yaml';
8
+ const DEFAULT_GLOB = ['**/*'];
9
+ const DEFAULT_IGNORES = [
10
+ '**/.git/**',
11
+ '**/.icopilot/**',
12
+ '**/node_modules/**',
13
+ '**/dist/**',
14
+ '**/build/**',
15
+ '**/coverage/**',
16
+ '**/.next/**',
17
+ '**/.turbo/**',
18
+ ];
19
+ const MAX_FILE_BYTES = 256 * 1024;
20
+ const MAX_RESULTS = 50;
21
+ const MAX_RESULTS_PER_REPO = 15;
22
+ export class MultiRepoOrchestrator {
23
+ rootDir = process.cwd();
24
+ configPath = path.join(this.rootDir, CONFIG_DIR, CONFIG_FILE);
25
+ repos = [];
26
+ currentRepoName;
27
+ loadConfig(rootDir) {
28
+ this.rootDir = path.resolve(rootDir);
29
+ this.configPath = path.join(this.rootDir, CONFIG_DIR, CONFIG_FILE);
30
+ this.repos = [];
31
+ this.currentRepoName = undefined;
32
+ if (!fs.existsSync(this.configPath)) {
33
+ return this.listRepos();
34
+ }
35
+ const raw = fs.readFileSync(this.configPath, 'utf8');
36
+ const document = parseDocument(raw);
37
+ if (document.errors.length > 0) {
38
+ const [firstError] = document.errors;
39
+ throw new Error(`Invalid YAML in ${path.relative(process.cwd(), this.configPath)}: ${firstError?.message ?? 'parse error'}`);
40
+ }
41
+ const parsed = document.toJSON();
42
+ const normalized = normalizeConfigFile(parsed, this.rootDir, this.configPath);
43
+ this.currentRepoName = normalized.current;
44
+ this.repos = normalized.repos;
45
+ return this.listRepos();
46
+ }
47
+ addRepo(config) {
48
+ const normalized = normalizeRepoConfig(config, this.rootDir, this.configPath);
49
+ const existingByName = this.repos.find((repo) => repo.name.toLowerCase() === normalized.name.toLowerCase());
50
+ if (existingByName) {
51
+ throw new Error(`repository already exists: ${existingByName.name}`);
52
+ }
53
+ const existingByPath = this.repos.find((repo) => samePath(repo.path, normalized.path));
54
+ if (existingByPath) {
55
+ throw new Error(`repository path already exists: ${existingByPath.name}`);
56
+ }
57
+ this.repos.push(normalized);
58
+ if (!this.currentRepoName) {
59
+ this.currentRepoName = normalized.name;
60
+ }
61
+ this.saveConfig();
62
+ return cloneRepo(normalized);
63
+ }
64
+ removeRepo(name) {
65
+ const index = this.repos.findIndex((repo) => repo.name.toLowerCase() === name.toLowerCase());
66
+ if (index === -1)
67
+ return false;
68
+ const [removed] = this.repos.splice(index, 1);
69
+ if (removed && this.currentRepoName?.toLowerCase() === removed.name.toLowerCase()) {
70
+ this.currentRepoName = this.repos[0]?.name;
71
+ }
72
+ this.saveConfig();
73
+ return true;
74
+ }
75
+ listRepos() {
76
+ return this.repos.map(cloneRepo);
77
+ }
78
+ switchRepo(name) {
79
+ const repo = this.repos.find((entry) => entry.name.toLowerCase() === name.toLowerCase());
80
+ if (!repo) {
81
+ throw new Error(`unknown repository: ${name}`);
82
+ }
83
+ this.currentRepoName = repo.name;
84
+ this.saveConfig();
85
+ return cloneRepo(repo);
86
+ }
87
+ async searchAcrossRepos(query) {
88
+ const needle = query.trim().toLowerCase();
89
+ if (!needle)
90
+ return [];
91
+ const hits = [];
92
+ for (const repo of this.repos) {
93
+ if (hits.length >= MAX_RESULTS)
94
+ break;
95
+ if (!fs.existsSync(repo.path) || !fs.statSync(repo.path).isDirectory())
96
+ continue;
97
+ const files = await fg(DEFAULT_GLOB, {
98
+ cwd: repo.path,
99
+ onlyFiles: true,
100
+ dot: false,
101
+ ignore: DEFAULT_IGNORES,
102
+ absolute: true,
103
+ suppressErrors: true,
104
+ });
105
+ let repoHits = 0;
106
+ for (const absolutePath of files) {
107
+ if (hits.length >= MAX_RESULTS || repoHits >= MAX_RESULTS_PER_REPO)
108
+ break;
109
+ const stat = safeStat(absolutePath);
110
+ if (!stat || stat.size > MAX_FILE_BYTES)
111
+ continue;
112
+ const raw = fs.readFileSync(absolutePath, 'utf8');
113
+ if (raw.includes('\u0000'))
114
+ continue;
115
+ const lines = raw.split(/\r?\n/);
116
+ for (let index = 0; index < lines.length; index += 1) {
117
+ const line = lines[index] ?? '';
118
+ if (!line.toLowerCase().includes(needle))
119
+ continue;
120
+ hits.push({
121
+ repo: repo.name,
122
+ file: path.relative(repo.path, absolutePath) || path.basename(absolutePath),
123
+ line: index + 1,
124
+ text: line.trim(),
125
+ absolutePath,
126
+ });
127
+ repoHits += 1;
128
+ if (hits.length >= MAX_RESULTS || repoHits >= MAX_RESULTS_PER_REPO)
129
+ break;
130
+ }
131
+ }
132
+ }
133
+ return hits;
134
+ }
135
+ async getStatus() {
136
+ const repos = await Promise.all(this.repos.map(async (repo) => getRepoStatus(repo)));
137
+ return {
138
+ rootDir: this.rootDir,
139
+ configPath: this.configPath,
140
+ current: this.currentRepoName,
141
+ repos,
142
+ };
143
+ }
144
+ async syncAll() {
145
+ return Promise.all(this.repos.map(async (repo) => syncRepo(repo)));
146
+ }
147
+ getCurrentRepoName() {
148
+ return this.currentRepoName;
149
+ }
150
+ getConfigPath() {
151
+ return this.configPath;
152
+ }
153
+ getRootDir() {
154
+ return this.rootDir;
155
+ }
156
+ saveConfig() {
157
+ fs.mkdirSync(path.dirname(this.configPath), { recursive: true });
158
+ const document = {
159
+ current: this.currentRepoName,
160
+ repos: this.repos.map((repo) => ({
161
+ ...repo,
162
+ path: toPortableRelativePath(this.rootDir, repo.path),
163
+ })),
164
+ };
165
+ fs.writeFileSync(this.configPath, stringify(document), 'utf8');
166
+ }
167
+ }
168
+ async function getRepoStatus(repo) {
169
+ const base = {
170
+ ...cloneRepo(repo),
171
+ exists: false,
172
+ git: false,
173
+ dirty: false,
174
+ ahead: 0,
175
+ behind: 0,
176
+ };
177
+ if (!fs.existsSync(repo.path) || !fs.statSync(repo.path).isDirectory()) {
178
+ return { ...base, error: 'path does not exist' };
179
+ }
180
+ base.exists = true;
181
+ const git = simpleGit(repo.path);
182
+ try {
183
+ base.git = await git.checkIsRepo();
184
+ if (!base.git) {
185
+ return base;
186
+ }
187
+ const branch = await git.revparse(['--abbrev-ref', 'HEAD']);
188
+ const status = await git.status();
189
+ return {
190
+ ...base,
191
+ dirty: status.files.length > 0,
192
+ ahead: status.ahead,
193
+ behind: status.behind,
194
+ currentBranch: branch.trim(),
195
+ };
196
+ }
197
+ catch (error) {
198
+ return {
199
+ ...base,
200
+ error: error instanceof Error ? error.message : String(error),
201
+ };
202
+ }
203
+ }
204
+ async function syncRepo(repo) {
205
+ const missing = !fs.existsSync(repo.path) || !fs.statSync(repo.path).isDirectory();
206
+ if (missing) {
207
+ return {
208
+ name: repo.name,
209
+ path: repo.path,
210
+ ok: false,
211
+ action: 'error',
212
+ message: 'path does not exist',
213
+ };
214
+ }
215
+ const git = simpleGit(repo.path);
216
+ try {
217
+ if (!(await git.checkIsRepo())) {
218
+ return {
219
+ name: repo.name,
220
+ path: repo.path,
221
+ ok: false,
222
+ action: 'skipped',
223
+ message: 'not a git repository',
224
+ };
225
+ }
226
+ const remotes = await git.getRemotes(true);
227
+ if (remotes.length === 0) {
228
+ return {
229
+ name: repo.name,
230
+ path: repo.path,
231
+ ok: true,
232
+ action: 'skipped',
233
+ message: 'no remotes configured',
234
+ };
235
+ }
236
+ await git.fetch(['--all', '--prune']);
237
+ const status = await git.status();
238
+ if (status.files.length > 0) {
239
+ return {
240
+ name: repo.name,
241
+ path: repo.path,
242
+ ok: true,
243
+ action: 'fetched',
244
+ message: 'fetched remotes; skipped pull because working tree is dirty',
245
+ };
246
+ }
247
+ const remoteName = repo.remote || remotes[0]?.name;
248
+ const branchName = repo.branch || (await git.revparse(['--abbrev-ref', 'HEAD'])).trim();
249
+ if (!remoteName || !branchName) {
250
+ return {
251
+ name: repo.name,
252
+ path: repo.path,
253
+ ok: true,
254
+ action: 'fetched',
255
+ message: 'fetched remotes',
256
+ };
257
+ }
258
+ await git.pull(remoteName, branchName, { '--ff-only': null });
259
+ return {
260
+ name: repo.name,
261
+ path: repo.path,
262
+ ok: true,
263
+ action: 'pulled',
264
+ message: `pulled ${remoteName}/${branchName}`,
265
+ };
266
+ }
267
+ catch (error) {
268
+ return {
269
+ name: repo.name,
270
+ path: repo.path,
271
+ ok: false,
272
+ action: 'error',
273
+ message: error instanceof Error ? error.message : String(error),
274
+ };
275
+ }
276
+ }
277
+ function normalizeConfigFile(value, rootDir, configPath) {
278
+ if (Array.isArray(value)) {
279
+ return {
280
+ repos: value.map((repo) => normalizeRepoConfig(repo, rootDir, configPath)),
281
+ };
282
+ }
283
+ if (!value || typeof value !== 'object') {
284
+ throw new Error(`${path.relative(process.cwd(), configPath)} must contain a YAML object or array`);
285
+ }
286
+ const record = value;
287
+ const reposValue = record.repos;
288
+ if (reposValue !== undefined && !Array.isArray(reposValue)) {
289
+ throw new Error(`${path.relative(process.cwd(), configPath)} field "repos" must be an array`);
290
+ }
291
+ const currentValue = record.current;
292
+ if (currentValue !== undefined &&
293
+ (typeof currentValue !== 'string' || currentValue.trim().length === 0)) {
294
+ throw new Error(`${path.relative(process.cwd(), configPath)} field "current" must be a non-empty string`);
295
+ }
296
+ return {
297
+ ...(typeof currentValue === 'string' ? { current: currentValue.trim() } : {}),
298
+ repos: (reposValue ?? []).map((repo) => normalizeRepoConfig(repo, rootDir, configPath)),
299
+ };
300
+ }
301
+ function normalizeRepoConfig(value, rootDir, configPath) {
302
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
303
+ throw new Error(`${path.relative(process.cwd(), configPath)} repository entries must be YAML objects`);
304
+ }
305
+ const record = value;
306
+ const name = requiredString(record.name, 'name', configPath);
307
+ const repoPath = requiredString(record.path, 'path', configPath);
308
+ const role = requiredString(record.role, 'role', configPath);
309
+ return {
310
+ name,
311
+ path: path.resolve(rootDir, repoPath),
312
+ remote: optionalString(record.remote, 'remote', configPath),
313
+ branch: optionalString(record.branch, 'branch', configPath),
314
+ role,
315
+ };
316
+ }
317
+ function requiredString(value, field, configPath) {
318
+ if (typeof value !== 'string' || value.trim().length === 0) {
319
+ throw new Error(`${path.relative(process.cwd(), configPath)} field "${field}" must be a non-empty string`);
320
+ }
321
+ return value.trim();
322
+ }
323
+ function optionalString(value, field, configPath) {
324
+ if (value === undefined || value === null || value === '')
325
+ return undefined;
326
+ if (typeof value !== 'string' || value.trim().length === 0) {
327
+ throw new Error(`${path.relative(process.cwd(), configPath)} field "${field}" must be a non-empty string`);
328
+ }
329
+ return value.trim();
330
+ }
331
+ function cloneRepo(repo) {
332
+ return { ...repo };
333
+ }
334
+ function toPortableRelativePath(rootDir, targetPath) {
335
+ const relative = path.relative(rootDir, targetPath);
336
+ if (!relative)
337
+ return '.';
338
+ return relative.split(path.sep).join('/');
339
+ }
340
+ function samePath(left, right) {
341
+ return path.normalize(left) === path.normalize(right);
342
+ }
343
+ function safeStat(filePath) {
344
+ try {
345
+ return fs.statSync(filePath);
346
+ }
347
+ catch {
348
+ return null;
349
+ }
350
+ }
@@ -0,0 +1,181 @@
1
+ import { activeProvider, client } from '../api/github-models.js';
2
+ import { getAgentConfig } from '../commands/agent-cmd.js';
3
+ import { aggregateResults } from './aggregator.js';
4
+ const DEFAULT_CONCURRENCY_LIMIT = 5;
5
+ const DEFAULT_TIMEOUT_MS = 60_000;
6
+ const BUILT_IN_AGENT_TYPES = new Set(['explore', 'task', 'review', 'plan']);
7
+ export class ParallelAgentRunner {
8
+ concurrencyLimit;
9
+ timeoutMs;
10
+ model;
11
+ onProgress;
12
+ executeTask;
13
+ constructor(options) {
14
+ this.model = options.model;
15
+ this.concurrencyLimit = normalizePositiveInt(options.concurrencyLimit, DEFAULT_CONCURRENCY_LIMIT);
16
+ this.timeoutMs = normalizePositiveInt(options.timeoutMs, DEFAULT_TIMEOUT_MS);
17
+ this.onProgress = options.onProgress;
18
+ this.executeTask = options.executeTask ?? defaultExecuteTask;
19
+ }
20
+ async runParallel(agents) {
21
+ if (!agents.length) {
22
+ return {
23
+ results: [],
24
+ aggregated: aggregateResults([]),
25
+ };
26
+ }
27
+ const total = agents.length;
28
+ let completed = 0;
29
+ const limit = createConcurrencyLimiter(this.concurrencyLimit);
30
+ for (const agent of agents) {
31
+ this.onProgress?.({
32
+ name: agent.name,
33
+ type: agent.type,
34
+ status: 'queued',
35
+ completed,
36
+ total,
37
+ });
38
+ }
39
+ const executions = agents.map((agent) => limit(async () => {
40
+ this.onProgress?.({
41
+ name: agent.name,
42
+ type: agent.type,
43
+ status: 'started',
44
+ completed,
45
+ total,
46
+ });
47
+ const result = await this.runSingle(agent);
48
+ completed += 1;
49
+ this.onProgress?.({
50
+ name: agent.name,
51
+ type: agent.type,
52
+ status: result.status,
53
+ completed,
54
+ total,
55
+ result,
56
+ });
57
+ return result;
58
+ }));
59
+ const settled = await Promise.allSettled(executions);
60
+ const results = settled.map((entry, index) => {
61
+ if (entry.status === 'fulfilled')
62
+ return entry.value;
63
+ return {
64
+ name: agents[index]?.name ?? `agent-${index + 1}`,
65
+ status: 'error',
66
+ output: formatError(entry.reason),
67
+ duration: 0,
68
+ };
69
+ });
70
+ return {
71
+ results,
72
+ aggregated: aggregateResults(results
73
+ .filter((result) => result.status === 'success')
74
+ .map((result) => ({
75
+ name: result.name,
76
+ output: result.output,
77
+ }))),
78
+ };
79
+ }
80
+ async runSingle(task) {
81
+ const startedAt = Date.now();
82
+ try {
83
+ const output = await this.executeWithTimeout(task);
84
+ return {
85
+ name: task.name,
86
+ status: 'success',
87
+ output,
88
+ duration: Date.now() - startedAt,
89
+ };
90
+ }
91
+ catch (error) {
92
+ return {
93
+ name: task.name,
94
+ status: 'error',
95
+ output: formatError(error),
96
+ duration: Date.now() - startedAt,
97
+ };
98
+ }
99
+ }
100
+ async executeWithTimeout(task) {
101
+ const controller = new AbortController();
102
+ let timedOut = false;
103
+ const timeout = setTimeout(() => {
104
+ timedOut = true;
105
+ controller.abort();
106
+ }, this.timeoutMs);
107
+ try {
108
+ return await this.executeTask(task, {
109
+ model: this.model,
110
+ signal: controller.signal,
111
+ systemPrompt: resolveSystemPrompt(task),
112
+ });
113
+ }
114
+ catch (error) {
115
+ if (timedOut) {
116
+ throw new Error(`Timed out after ${this.timeoutMs}ms`);
117
+ }
118
+ throw error;
119
+ }
120
+ finally {
121
+ clearTimeout(timeout);
122
+ }
123
+ }
124
+ }
125
+ async function defaultExecuteTask(task, options) {
126
+ const provider = activeProvider();
127
+ const messages = [
128
+ { role: 'system', content: options.systemPrompt },
129
+ { role: 'user', content: task.prompt },
130
+ ];
131
+ const response = await client().chat.completions.create({
132
+ model: options.model,
133
+ messages,
134
+ temperature: 0.2,
135
+ ...(provider?.maxTokens ? { max_tokens: provider.maxTokens } : {}),
136
+ }, { signal: options.signal });
137
+ return response.choices[0]?.message?.content?.trim() || '';
138
+ }
139
+ function resolveSystemPrompt(task) {
140
+ const inlinePrompt = task.systemPrompt?.trim();
141
+ if (inlinePrompt)
142
+ return inlinePrompt;
143
+ if (BUILT_IN_AGENT_TYPES.has(task.type)) {
144
+ return getAgentConfig(task.type).systemPrompt;
145
+ }
146
+ return 'You are a specialized sub-agent. Complete the assigned task accurately and concisely.';
147
+ }
148
+ function createConcurrencyLimiter(limit) {
149
+ let activeCount = 0;
150
+ const queue = [];
151
+ const next = () => {
152
+ activeCount -= 1;
153
+ const resume = queue.shift();
154
+ resume?.();
155
+ };
156
+ return async function runLimited(fn) {
157
+ if (activeCount >= limit) {
158
+ await new Promise((resolve) => queue.push(resolve));
159
+ }
160
+ activeCount += 1;
161
+ try {
162
+ return await fn();
163
+ }
164
+ finally {
165
+ next();
166
+ }
167
+ };
168
+ }
169
+ function normalizePositiveInt(value, fallback) {
170
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 1) {
171
+ return fallback;
172
+ }
173
+ return Math.floor(value);
174
+ }
175
+ function formatError(error) {
176
+ if (error instanceof Error)
177
+ return error.message;
178
+ if (typeof error === 'string')
179
+ return error;
180
+ return 'Unknown error';
181
+ }
@@ -0,0 +1,144 @@
1
+ const DEFAULT_THRESHOLD = 0.7;
2
+ const KEYWORD_BOOST = 0.12;
3
+ const QUESTION_BOOST = 0.16;
4
+ const STARTS_WITH_BOOST = 0.08;
5
+ const DEFAULT_ROUTES = [
6
+ {
7
+ pattern: ['review', 'code review', 'audit', 'inspect'],
8
+ agentType: 'review',
9
+ confidence: 0.5,
10
+ },
11
+ {
12
+ pattern: ['explain', 'how', 'why', 'what does'],
13
+ agentType: 'explain',
14
+ confidence: 0.48,
15
+ },
16
+ {
17
+ pattern: ['fix', 'error', 'bug', 'broken', 'failing', 'debug'],
18
+ agentType: 'fix',
19
+ confidence: 0.52,
20
+ },
21
+ {
22
+ pattern: ['refactor', 'improve', 'cleanup', 'clean up', 'optimize', 'simplify'],
23
+ agentType: 'refactor',
24
+ confidence: 0.5,
25
+ },
26
+ {
27
+ pattern: ['test', 'tests', 'spec', 'specs'],
28
+ agentType: 'test',
29
+ confidence: 0.5,
30
+ },
31
+ {
32
+ pattern: ['explore', 'find', 'where', 'locate', 'search'],
33
+ agentType: 'explore',
34
+ confidence: 0.48,
35
+ },
36
+ {
37
+ pattern: ['plan', 'design', 'architect', 'architecture', 'approach'],
38
+ agentType: 'plan',
39
+ confidence: 0.5,
40
+ },
41
+ ];
42
+ const QUESTION_PATTERNS = {
43
+ review: [/\breview\b/, /\baudit\b/, /\binspect\b/],
44
+ explain: [/^(?:can you\s+)?(?:explain|how|why)\b/, /\bwhat does\b/, /\bhow does\b/],
45
+ fix: [/^(?:please\s+)?(?:fix|debug)\b/, /\b(?:error|bug|broken|failing)\b/],
46
+ refactor: [/^(?:please\s+)?(?:refactor|improve)\b/, /\bclean up\b/, /\boptimi[sz]e\b/],
47
+ test: [/^(?:add|write|create)?\s*tests?\b/, /\bspecs?\b/, /\btest\b/],
48
+ explore: [/^(?:where|find|locate|explore)\b/, /\bwhere is\b/, /\bfind\b/],
49
+ plan: [/^(?:plan|design|architect)\b/, /\barchitecture\b/, /\bhow should we\b/],
50
+ };
51
+ const STARTS_WITH_PATTERNS = {
52
+ review: [/^(?:please\s+)?review\b/],
53
+ explain: [/^(?:can you\s+)?(?:explain|how|why)\b/],
54
+ fix: [/^(?:please\s+)?(?:fix|debug)\b/],
55
+ refactor: [/^(?:please\s+)?(?:refactor|improve)\b/],
56
+ test: [/^(?:please\s+)?(?:test|spec|add tests?|write tests?)\b/],
57
+ explore: [/^(?:please\s+)?(?:explore|find|where|locate)\b/],
58
+ plan: [/^(?:please\s+)?(?:plan|design|architect)\b/],
59
+ };
60
+ const customRoutes = [];
61
+ let threshold = readThreshold(process.env.ICOPILOT_AGENT_ROUTE_THRESHOLD);
62
+ export function routeQuery(query) {
63
+ const normalizedQuery = normalizeQuery(query);
64
+ if (!normalizedQuery) {
65
+ return null;
66
+ }
67
+ let bestMatch = null;
68
+ for (const route of [...customRoutes, ...DEFAULT_ROUTES]) {
69
+ const score = scoreRoute(route, normalizedQuery);
70
+ if (!score) {
71
+ continue;
72
+ }
73
+ if (!bestMatch || score > bestMatch.confidence) {
74
+ bestMatch = { agent: route.agentType, confidence: score };
75
+ }
76
+ }
77
+ if (!bestMatch || bestMatch.confidence < threshold) {
78
+ return null;
79
+ }
80
+ return bestMatch;
81
+ }
82
+ export function addRoute(route) {
83
+ customRoutes.unshift({
84
+ ...route,
85
+ confidence: clampConfidence(route.confidence),
86
+ });
87
+ }
88
+ export function setRouteThreshold(nextThreshold) {
89
+ threshold = clampConfidence(nextThreshold);
90
+ }
91
+ export function getRouteThreshold() {
92
+ return threshold;
93
+ }
94
+ function scoreRoute(route, normalizedQuery) {
95
+ const keywordMatches = countMatches(route.pattern, normalizedQuery);
96
+ if (keywordMatches === 0) {
97
+ return 0;
98
+ }
99
+ let score = clampConfidence(route.confidence);
100
+ score += Math.min(0.24, keywordMatches * KEYWORD_BOOST);
101
+ score += Math.min(0.24, countSignalMatches(route.agentType, normalizedQuery, QUESTION_PATTERNS) * QUESTION_BOOST);
102
+ if (hasStartsWithSignal(route.agentType, normalizedQuery)) {
103
+ score += STARTS_WITH_BOOST;
104
+ }
105
+ return roundConfidence(score);
106
+ }
107
+ function countMatches(pattern, normalizedQuery) {
108
+ if (pattern instanceof RegExp) {
109
+ const flags = pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`;
110
+ const matcher = new RegExp(pattern.source, flags);
111
+ return [...normalizedQuery.matchAll(matcher)].length;
112
+ }
113
+ return pattern.reduce((count, keyword) => count + (hasKeywordMatch(keyword, normalizedQuery) ? 1 : 0), 0);
114
+ }
115
+ function countSignalMatches(agentType, normalizedQuery, patternsByAgent) {
116
+ return (patternsByAgent[agentType] ?? []).reduce((count, pattern) => count + (pattern.test(normalizedQuery) ? 1 : 0), 0);
117
+ }
118
+ function hasStartsWithSignal(agentType, normalizedQuery) {
119
+ return (STARTS_WITH_PATTERNS[agentType] ?? []).some((pattern) => pattern.test(normalizedQuery));
120
+ }
121
+ function hasKeywordMatch(keyword, normalizedQuery) {
122
+ const escapedKeyword = escapeRegExp(keyword.toLowerCase());
123
+ const keywordPattern = new RegExp(`(?:^|\\b)${escapedKeyword}(?=\\b|$)`);
124
+ return keywordPattern.test(normalizedQuery);
125
+ }
126
+ function normalizeQuery(query) {
127
+ return query.trim().toLowerCase().replace(/\s+/g, ' ');
128
+ }
129
+ function readThreshold(value) {
130
+ const parsed = Number(value);
131
+ return Number.isFinite(parsed) ? clampConfidence(parsed) : DEFAULT_THRESHOLD;
132
+ }
133
+ function clampConfidence(value) {
134
+ if (!Number.isFinite(value)) {
135
+ return DEFAULT_THRESHOLD;
136
+ }
137
+ return Math.min(1, Math.max(0, value));
138
+ }
139
+ function roundConfidence(value) {
140
+ return Math.round(clampConfidence(value) * 100) / 100;
141
+ }
142
+ function escapeRegExp(value) {
143
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
144
+ }