@oh-my-pi/pi-coding-agent 15.7.6 → 15.8.2

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 (117) hide show
  1. package/CHANGELOG.md +172 -198
  2. package/dist/types/async/job-manager.d.ts +3 -3
  3. package/dist/types/cli/args.d.ts +1 -0
  4. package/dist/types/cli/claude-trace-cli.d.ts +54 -0
  5. package/dist/types/cli/session-picker.d.ts +10 -3
  6. package/dist/types/cli/update-cli.d.ts +17 -0
  7. package/dist/types/commands/launch.d.ts +3 -0
  8. package/dist/types/config/keybindings.d.ts +10 -1
  9. package/dist/types/config/settings-schema.d.ts +2 -2
  10. package/dist/types/config/settings.d.ts +13 -0
  11. package/dist/types/edit/index.d.ts +6 -0
  12. package/dist/types/edit/streaming.d.ts +8 -0
  13. package/dist/types/eval/concurrency-bridge.d.ts +26 -0
  14. package/dist/types/eval/js/tool-bridge.d.ts +2 -1
  15. package/dist/types/export/ttsr.d.ts +9 -0
  16. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +11 -0
  17. package/dist/types/main.d.ts +5 -0
  18. package/dist/types/mcp/transports/stdio.d.ts +19 -0
  19. package/dist/types/modes/components/custom-editor.d.ts +3 -1
  20. package/dist/types/modes/components/hook-selector.d.ts +3 -0
  21. package/dist/types/modes/components/session-selector.d.ts +32 -5
  22. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  23. package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -2
  24. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  25. package/dist/types/modes/interactive-mode.d.ts +9 -2
  26. package/dist/types/modes/types.d.ts +4 -2
  27. package/dist/types/registry/agent-registry.d.ts +1 -1
  28. package/dist/types/sdk.d.ts +2 -2
  29. package/dist/types/session/agent-session.d.ts +4 -2
  30. package/dist/types/session/history-storage.d.ts +16 -1
  31. package/dist/types/session/session-manager.d.ts +4 -0
  32. package/dist/types/task/output-manager.d.ts +6 -15
  33. package/dist/types/tools/find.d.ts +0 -9
  34. package/dist/types/tools/index.d.ts +1 -1
  35. package/dist/types/tools/path-utils.d.ts +16 -0
  36. package/dist/types/tools/sqlite-reader.d.ts +25 -8
  37. package/dist/types/tools/write.d.ts +2 -0
  38. package/dist/types/utils/clipboard.d.ts +4 -0
  39. package/dist/types/utils/jj.d.ts +49 -0
  40. package/dist/types/web/kagi.d.ts +76 -0
  41. package/dist/types/web/search/providers/exa.d.ts +7 -1
  42. package/dist/types/web/search/providers/kagi.d.ts +1 -0
  43. package/package.json +9 -9
  44. package/src/async/job-manager.ts +3 -3
  45. package/src/cli/args.ts +6 -2
  46. package/src/cli/claude-trace-cli.ts +783 -0
  47. package/src/cli/session-picker.ts +36 -10
  48. package/src/cli/update-cli.ts +35 -2
  49. package/src/commands/launch.ts +3 -0
  50. package/src/config/keybindings.ts +14 -1
  51. package/src/config/model-registry.ts +18 -7
  52. package/src/config/settings-schema.ts +2 -2
  53. package/src/config/settings.ts +23 -0
  54. package/src/discovery/builtin-rules/index.ts +2 -0
  55. package/src/discovery/builtin-rules/ts-no-deprecated-leftovers.md +44 -0
  56. package/src/discovery/claude-plugins.ts +7 -9
  57. package/src/edit/index.ts +10 -0
  58. package/src/edit/streaming.ts +65 -0
  59. package/src/eval/__tests__/agent-bridge.test.ts +58 -4
  60. package/src/eval/concurrency-bridge.ts +34 -0
  61. package/src/eval/js/shared/prelude.txt +20 -17
  62. package/src/eval/js/tool-bridge.ts +5 -0
  63. package/src/eval/py/prelude.py +23 -15
  64. package/src/export/ttsr.ts +18 -1
  65. package/src/extensibility/custom-commands/bundled/review/index.ts +74 -45
  66. package/src/extensibility/plugins/legacy-pi-compat.ts +115 -131
  67. package/src/extensibility/skills.ts +0 -1
  68. package/src/internal-urls/docs-index.generated.ts +12 -11
  69. package/src/main.ts +92 -24
  70. package/src/mcp/transports/stdio.ts +55 -22
  71. package/src/modes/acp/acp-event-mapper.ts +54 -4
  72. package/src/modes/components/custom-editor.ts +10 -0
  73. package/src/modes/components/hook-selector.ts +89 -31
  74. package/src/modes/components/oauth-selector.ts +12 -6
  75. package/src/modes/components/session-selector.ts +179 -24
  76. package/src/modes/components/tool-execution.ts +16 -3
  77. package/src/modes/controllers/command-controller.ts +2 -11
  78. package/src/modes/controllers/extension-ui-controller.ts +3 -2
  79. package/src/modes/controllers/input-controller.ts +19 -1
  80. package/src/modes/controllers/selector-controller.ts +61 -21
  81. package/src/modes/interactive-mode.ts +125 -15
  82. package/src/modes/types.ts +5 -2
  83. package/src/prompts/agents/reviewer.md +2 -2
  84. package/src/prompts/review-request.md +1 -1
  85. package/src/prompts/system/empty-stop-retry.md +6 -0
  86. package/src/prompts/system/orchestrate-notice.md +5 -3
  87. package/src/prompts/system/workflow-notice.md +2 -2
  88. package/src/prompts/tools/eval.md +5 -5
  89. package/src/prompts/tools/find.md +1 -1
  90. package/src/prompts/tools/irc.md +6 -6
  91. package/src/prompts/tools/search-tool-bm25.md +9 -2
  92. package/src/prompts/tools/search.md +1 -1
  93. package/src/prompts/tools/task.md +1 -1
  94. package/src/registry/agent-registry.ts +1 -1
  95. package/src/sdk.ts +85 -31
  96. package/src/session/agent-session.ts +209 -54
  97. package/src/session/history-storage.ts +56 -12
  98. package/src/session/session-manager.ts +34 -0
  99. package/src/task/output-manager.ts +40 -48
  100. package/src/task/render.ts +3 -8
  101. package/src/tools/browser/tab-worker.ts +8 -5
  102. package/src/tools/find.ts +5 -29
  103. package/src/tools/index.ts +1 -1
  104. package/src/tools/path-utils.ts +144 -1
  105. package/src/tools/read.ts +47 -0
  106. package/src/tools/search-tool-bm25.ts +7 -1
  107. package/src/tools/search.ts +2 -27
  108. package/src/tools/sqlite-reader.ts +92 -9
  109. package/src/tools/write.ts +6 -0
  110. package/src/utils/clipboard.ts +38 -1
  111. package/src/utils/git.ts +19 -23
  112. package/src/utils/jj.ts +225 -0
  113. package/src/utils/open.ts +37 -2
  114. package/src/web/kagi.ts +168 -49
  115. package/src/web/search/providers/anthropic.ts +1 -1
  116. package/src/web/search/providers/exa.ts +20 -86
  117. package/src/web/search/providers/kagi.ts +4 -0
@@ -1,5 +1,4 @@
1
1
  import * as fs from "node:fs/promises";
2
- import * as os from "node:os";
3
2
  import * as path from "node:path";
4
3
  import * as url from "node:url";
5
4
  import { isCompiledBinary } from "@oh-my-pi/pi-utils";
@@ -45,8 +44,6 @@ const LEGACY_PI_IMPORT_SPECIFIER_REGEX = new RegExp(
45
44
  `((?:from\\s+|import\\s*\\(\\s*)["'])(@(?:${PI_SCOPE_ALTERNATION})/(?:${PI_PACKAGE_ALTERNATION})(?:/[^"'()\\s]+)?)(["'])`,
46
45
  "g",
47
46
  );
48
- const LEGACY_PI_FILE_PREFIX = "omp-legacy-pi-file:";
49
- const LEGACY_PI_FILE_NAMESPACE = "omp-legacy-pi-file";
50
47
  const resolvedSpecifierFallbacks = new Map<string, string>();
51
48
 
52
49
  // Extensions that imported `@sinclair/typebox` directly used to resolve against a
@@ -56,7 +53,6 @@ const resolvedSpecifierFallbacks = new Map<string, string>();
56
53
  // changes. Submodules like `@sinclair/typebox/compiler` are intentionally not
57
54
  // remapped — those expose TypeBox-only APIs the shim does not provide and plugins
58
55
  // relying on them must vendor `@sinclair/typebox` directly.
59
- const TYPEBOX_SPECIFIER = "@sinclair/typebox";
60
56
  const TYPEBOX_SPECIFIER_FILTER = /^@sinclair\/typebox$/;
61
57
 
62
58
  // Compat shim and bundled-package paths used in compiled-binary mode. The shim
@@ -214,129 +210,146 @@ function rewriteLegacyPiImports(source: string): string {
214
210
  } catch {
215
211
  // Resolution failed — typically in compiled binary mode where
216
212
  // Bun.resolveSync cannot walk up from /$bunfs/root to find the
217
- // bundled node_modules. Return the original specifier unchanged so
218
- // rewriteBareImportsForLegacyExtension can resolve it against the
219
- // plugin's own installed peer deps instead.
213
+ // bundled node_modules. Leave the specifier unchanged so Bun
214
+ // resolves it natively against the extension's own peer deps.
220
215
  return match;
221
216
  }
222
217
  },
223
218
  );
224
219
  }
225
220
 
226
- // Match static `from "..."` / `from '...'` import specifiers.
227
- const STATIC_IMPORT_SPECIFIER_REGEX = /(from\s+["'])([^"']+)(["'])/g;
228
- // Match static imports plus dynamic `import("...")` / `import('...')` specifiers.
229
- const ANY_IMPORT_SPECIFIER_REGEX = /((?:from\s+|import\s*\(\s*)["'])([^"']+)(["'])/g;
230
-
231
- /** Resolve bare imports against the extension directory before loading mirrored legacy Pi files. */
232
- function isUrlLikeSpecifier(specifier: string): boolean {
233
- // Windows drive-letter paths (e.g. `C:\foo` or `C:/foo`) also match the URL
234
- // scheme shape `[A-Za-z][A-Za-z\d+.-]*:`. Treat them as filesystem paths so
235
- // `toRewrittenImportSpecifier` converts them to `file://` URLs instead of
236
- // emitting raw paths whose `\n`, `\U`, ... get eaten by TS string-literal
237
- // escapes inside the mirrored extension file.
238
- if (/^[a-zA-Z]:[\\/]/.test(specifier)) return false;
239
- return /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(specifier);
240
- }
221
+ // Match the bare `@sinclair/typebox` import specifier (static + dynamic).
222
+ // Subpath imports like `@sinclair/typebox/compiler` are intentionally excluded —
223
+ // they expose TypeBox-only APIs the Zod-backed shim does not provide.
224
+ const TYPEBOX_IMPORT_SPECIFIER_REGEX = /((?:from\s+|import\s*\(\s*)["'])(@sinclair\/typebox)(["'])/g;
241
225
 
242
- function shouldPreserveImportSpecifier(specifier: string): boolean {
243
- return specifier.startsWith(".") || path.isAbsolute(specifier) || isUrlLikeSpecifier(specifier);
226
+ /**
227
+ * Rewrite the legacy specifiers a Pi extension may import — `@(scope)/pi-*` and
228
+ * the bare `@sinclair/typebox` root — to absolute `file://` URLs pointing at the
229
+ * bundled package or compat shim. Every other specifier (relative siblings, the
230
+ * extension's own bare dependencies) is left untouched so Bun resolves it
231
+ * natively from the extension's real on-disk location.
232
+ */
233
+ function rewriteLegacyExtensionSource(source: string): string {
234
+ const withPi = rewriteLegacyPiImports(source);
235
+ return withPi.replace(
236
+ TYPEBOX_IMPORT_SPECIFIER_REGEX,
237
+ (_match, prefix: string, _specifier: string, suffix: string) => {
238
+ return `${prefix}${toImportSpecifier(TYPEBOX_SHIM_PATH)}${suffix}`;
239
+ },
240
+ );
244
241
  }
245
242
 
246
- function toRewrittenImportSpecifier(resolvedPath: string): string {
247
- return isUrlLikeSpecifier(resolvedPath) ? resolvedPath : toImportSpecifier(resolvedPath);
243
+ function escapeRegExp(value: string): string {
244
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
248
245
  }
249
246
 
250
- function rewriteBareImportsForLegacyExtension(source: string, importerPath: string): string {
251
- const importerDir = path.dirname(importerPath);
252
- return source.replace(ANY_IMPORT_SPECIFIER_REGEX, (match, prefix: string, specifier: string, suffix: string) => {
253
- // Skip relative, absolute, URL-style, and already-resolved Node specifiers.
254
- if (shouldPreserveImportSpecifier(specifier)) {
255
- return match;
256
- }
257
- if (specifier === TYPEBOX_SPECIFIER) {
258
- return `${prefix}${toRewrittenImportSpecifier(TYPEBOX_SHIM_PATH)}${suffix}`;
259
- }
260
- try {
261
- const resolved = Bun.resolveSync(specifier, importerDir);
262
- return `${prefix}${toRewrittenImportSpecifier(resolved)}${suffix}`;
263
- } catch {
264
- return match;
265
- }
266
- });
267
- }
247
+ // Match relative import specifiers (static `from "./…"` and dynamic
248
+ // `import("./…")`). Used to walk an extension's own module graph; bare and
249
+ // absolute specifiers are deliberately excluded.
250
+ const RELATIVE_IMPORT_SPECIFIER_REGEX = /(?:from\s+|import\s*\(\s*)["'](\.\.?\/[^"']+)["']/g;
268
251
 
269
- interface LegacyPiMirrorState {
270
- root: string;
271
- seen: Map<string, string>;
272
- }
252
+ // Extension entry realpaths that already have a load-time rewrite hook
253
+ // installed. Each `Bun.plugin()` registration is process-global and permanent,
254
+ // so we register at most one hook per entry.
255
+ const hookedExtensionEntries = new Set<string>();
273
256
 
274
- function getMirrorPath(sourcePath: string, state: LegacyPiMirrorState): string {
275
- const extension = path.extname(sourcePath) || ".js";
276
- const digest = Bun.hash(sourcePath).toString(36);
277
- return path.join(state.root, `module-${digest}${extension}`);
257
+ /** Resolve symlinks in a path, falling back to the input if realpath fails. */
258
+ async function realpathOrSelf(p: string): Promise<string> {
259
+ try {
260
+ return await fs.realpath(p);
261
+ } catch {
262
+ return p;
263
+ }
278
264
  }
279
265
 
280
- async function rewriteRelativeImportsForLegacyExtension(
281
- source: string,
282
- importerPath: string,
283
- state: LegacyPiMirrorState,
284
- ): Promise<string> {
285
- const replacements = new Map<string, string>();
286
-
287
- for (const match of source.matchAll(STATIC_IMPORT_SPECIFIER_REGEX)) {
288
- const specifier = match[2];
289
- if (!specifier.startsWith("./") && !specifier.startsWith("../")) {
266
+ /**
267
+ * Walk the extension's relative-import graph starting at `entryRealPath`,
268
+ * returning the realpath of every reachable source module. Only relative
269
+ * specifiers (`./`, `../`) are followed — bare and absolute imports are left to
270
+ * Bun's native resolver — so the set is exactly the extension's own source,
271
+ * wherever it physically lives (a `../src` sibling, a symlinked sub-tree, …).
272
+ * This mirrors the module set the old temp-dir mirror tracked, minus the copy.
273
+ */
274
+ async function collectExtensionModules(entryRealPath: string): Promise<Set<string>> {
275
+ const modules = new Set<string>();
276
+ const queue = [entryRealPath];
277
+ while (queue.length > 0) {
278
+ const file = queue.pop();
279
+ if (!file || modules.has(file)) {
290
280
  continue;
291
281
  }
292
-
293
- const resolved = Bun.resolveSync(specifier, path.dirname(importerPath));
294
- const mirrored = await mirrorLegacyPiFile(resolved, state);
295
- replacements.set(specifier, toImportSpecifier(mirrored));
296
- }
297
-
298
- if (replacements.size === 0) {
299
- return source;
282
+ let source: string;
283
+ try {
284
+ source = await Bun.file(file).text();
285
+ } catch {
286
+ continue;
287
+ }
288
+ modules.add(file);
289
+ const dir = path.dirname(file);
290
+ for (const match of source.matchAll(RELATIVE_IMPORT_SPECIFIER_REGEX)) {
291
+ try {
292
+ const resolved = await realpathOrSelf(Bun.resolveSync(match[1], dir));
293
+ if (!modules.has(resolved)) {
294
+ queue.push(resolved);
295
+ }
296
+ } catch {
297
+ // Unresolvable relative import (e.g. a type-only path); skip it.
298
+ }
299
+ }
300
300
  }
301
-
302
- return source.replace(STATIC_IMPORT_SPECIFIER_REGEX, (match, prefix: string, specifier: string, suffix: string) => {
303
- const replacement = replacements.get(specifier);
304
- return replacement ? `${prefix}${replacement}${suffix}` : match;
305
- });
301
+ return modules;
306
302
  }
307
303
 
308
- async function rewriteLegacyPiImportsForRuntime(
309
- source: string,
310
- importerPath: string,
311
- state: LegacyPiMirrorState,
312
- ): Promise<string> {
313
- const withRelativeResolved = await rewriteRelativeImportsForLegacyExtension(source, importerPath, state);
314
- const withLegacyRemap = rewriteLegacyPiImports(withRelativeResolved);
315
- return rewriteBareImportsForLegacyExtension(withLegacyRemap, importerPath);
316
- }
317
-
318
- async function mirrorLegacyPiFile(sourcePath: string, state: LegacyPiMirrorState): Promise<string> {
319
- const resolvedPath = path.resolve(sourcePath);
320
- const cached = state.seen.get(resolvedPath);
321
- if (cached) {
322
- return cached;
304
+ /**
305
+ * Install a `Bun.plugin()` `onLoad` hook scoped to exactly the modules in an
306
+ * extension's relative-import graph, so their legacy `@(scope)/pi-*` and bare
307
+ * `@sinclair/typebox` imports are rewritten at load time. A runtime `onLoad`
308
+ * cannot fall through (Bun requires a result object), so the filter is an
309
+ * exact-path alternation of the graph's realpaths — it never matches the host,
310
+ * other extensions, `node_modules` deps, or unrelated project source.
311
+ */
312
+ async function ensureExtensionGraphHook(entryRealPath: string): Promise<void> {
313
+ if (hookedExtensionEntries.has(entryRealPath)) {
314
+ return;
323
315
  }
316
+ hookedExtensionEntries.add(entryRealPath);
324
317
 
325
- const mirrorPath = getMirrorPath(resolvedPath, state);
326
- state.seen.set(resolvedPath, mirrorPath);
327
-
328
- const raw = await Bun.file(resolvedPath).text();
329
- const rewritten = await rewriteLegacyPiImportsForRuntime(raw, resolvedPath, state);
330
- await Bun.write(mirrorPath, rewritten);
331
- return mirrorPath;
318
+ const modules = await collectExtensionModules(entryRealPath);
319
+ const alternation = [...modules].map(escapeRegExp).join("|");
320
+ const filter = new RegExp(`^(?:${alternation})$`);
321
+ Bun.plugin({
322
+ name: `omp:legacy-pi-ext:${Bun.hash(entryRealPath).toString(36)}`,
323
+ setup(build) {
324
+ build.onLoad({ filter, namespace: "file" }, async args => {
325
+ // Re-read on every load so a `?mtime` reload picks up edited source.
326
+ const raw = await Bun.file(args.path).text();
327
+ return { contents: rewriteLegacyExtensionSource(raw), loader: getLoader(args.path) };
328
+ });
329
+ },
330
+ });
332
331
  }
333
332
 
333
+ /**
334
+ * Load a legacy Pi extension module from its real on-disk location.
335
+ *
336
+ * The extension runs in place, so its `import.meta.url` is the real source file
337
+ * and `__dirname`-relative `readFileSync` asset loads (HTML/CSS bundled next to
338
+ * the entry) resolve exactly as they do under the original Pi runtime — no
339
+ * temp-directory mirroring and no asset copying. An `onLoad` hook scoped to the
340
+ * entry's relative-import graph rewrites only the legacy `@(scope)/pi-*` and
341
+ * `@sinclair/typebox` imports in the extension's own source; everything else
342
+ * resolves natively.
343
+ */
334
344
  export async function loadLegacyPiModule(resolvedPath: string): Promise<unknown> {
335
- const root = path.join(os.tmpdir(), "omp-legacy-pi-file", `entry-${Bun.hash(resolvedPath).toString(36)}`);
336
- await fs.rm(root, { recursive: true, force: true });
337
- const state: LegacyPiMirrorState = { root, seen: new Map() };
338
- const mirroredEntry = await mirrorLegacyPiFile(resolvedPath, state);
339
- return import(`${toImportSpecifier(mirroredEntry)}?mtime=${Date.now()}`);
345
+ // Bun reports the realpath of a loaded module to `onLoad` and exposes it as
346
+ // `import.meta.url`. Resolve symlinks here too (macOS `/var`→`/private/var`,
347
+ // `bun link`/pnpm installs) so the rewrite filter matches the path Bun
348
+ // actually hands the hook.
349
+ const entryRealPath = await realpathOrSelf(path.resolve(resolvedPath));
350
+ await ensureExtensionGraphHook(entryRealPath);
351
+ // `?mtime` busts Bun's module cache so repeat loads pick up edited source.
352
+ return import(`${toImportSpecifier(entryRealPath)}?mtime=${Date.now()}`);
340
353
  }
341
354
 
342
355
  function getLoader(path: string): "js" | "jsx" | "ts" | "tsx" {
@@ -396,36 +409,7 @@ export function installLegacyPiSpecifierShim(): void {
396
409
  name: "omp:legacy-pi-shim",
397
410
  setup(build) {
398
411
  build.onResolve({ filter: LEGACY_PI_SPECIFIER_FILTER, namespace: "file" }, resolveLegacyPiSpecifier);
399
- build.onResolve(
400
- { filter: LEGACY_PI_SPECIFIER_FILTER, namespace: LEGACY_PI_FILE_NAMESPACE },
401
- resolveLegacyPiSpecifier,
402
- );
403
-
404
412
  build.onResolve({ filter: TYPEBOX_SPECIFIER_FILTER, namespace: "file" }, resolveTypeBoxSpecifier);
405
- build.onResolve(
406
- { filter: TYPEBOX_SPECIFIER_FILTER, namespace: LEGACY_PI_FILE_NAMESPACE },
407
- resolveTypeBoxSpecifier,
408
- );
409
-
410
- build.onResolve({ filter: /^omp-legacy-pi-file:/, namespace: "file" }, args => ({
411
- path: args.path.slice(LEGACY_PI_FILE_PREFIX.length),
412
- namespace: LEGACY_PI_FILE_NAMESPACE,
413
- }));
414
-
415
- build.onResolve({ filter: /^(?:\.{1,2}\/|\/)/, namespace: LEGACY_PI_FILE_NAMESPACE }, args => ({
416
- path: args.path.startsWith("/") ? args.path : Bun.resolveSync(args.path, path.dirname(args.importer)),
417
- namespace: LEGACY_PI_FILE_NAMESPACE,
418
- }));
419
-
420
- build.onLoad({ filter: /\.[cm]?[jt]sx?$/, namespace: LEGACY_PI_FILE_NAMESPACE }, async args => {
421
- const raw = await Bun.file(args.path).text();
422
- const withLegacyRemap = rewriteLegacyPiImports(raw);
423
- const withBareResolved = rewriteBareImportsForLegacyExtension(withLegacyRemap, args.path);
424
- return {
425
- contents: withBareResolved,
426
- loader: getLoader(args.path),
427
- };
428
- });
429
413
  },
430
414
  });
431
415
  }
@@ -273,7 +273,6 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
273
273
  const skills = Array.from(skillMap.values());
274
274
  // Deterministic ordering for prompt stability (case-insensitive, then exact name, then path).
275
275
  skills.sort((a, b) => compareSkillOrder(a.name, a.filePath, b.name, b.filePath));
276
-
277
276
  return {
278
277
  skills,
279
278
  warnings: [...(result.warnings ?? []).map(w => ({ skillPath: "", message: w })), ...collisionWarnings],