@yemi33/minions 0.1.1680 → 0.1.1682

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.
package/engine/queries.js CHANGED
@@ -722,31 +722,120 @@ function buildPrUrlFromId(prId, pr, projects) {
722
722
 
723
723
  // ── Skills ──────────────────────────────────────────────────────────────────
724
724
 
725
+ // Walk a `skills/` dir and push SKILL.md entries — handles both flat
726
+ // (skills/SKILL.md → pluginName) and nested (skills/<entry>/SKILL.md →
727
+ // pluginName:entry) layouts. Used by both Claude and Copilot plugin scans.
728
+ function _collectPluginSkillsDir(skillsDir, pluginName, scope, seenSet, out) {
729
+ let entries;
730
+ try { entries = fs.readdirSync(skillsDir, { withFileTypes: true }); } catch { return; }
731
+ for (const dirent of entries) {
732
+ const entry = dirent.name;
733
+ const entryPath = path.join(skillsDir, entry);
734
+ if (entry === 'SKILL.md') {
735
+ if (!seenSet.has(pluginName)) {
736
+ out.push({ file: 'SKILL.md', dir: skillsDir, scope, skillName: pluginName });
737
+ seenSet.add(pluginName);
738
+ }
739
+ } else if (dirent.isDirectory()) {
740
+ const nestedSkill = path.join(entryPath, 'SKILL.md');
741
+ if (fs.existsSync(nestedSkill)) {
742
+ const name = pluginName + ':' + entry;
743
+ if (!seenSet.has(name)) {
744
+ out.push({ file: 'SKILL.md', dir: entryPath, scope, skillName: name });
745
+ seenSet.add(name);
746
+ }
747
+ }
748
+ }
749
+ }
750
+ }
751
+
752
+ function _collectNativeSkillsDir(skillsDir, scope, seenSet, out, extra = {}) {
753
+ let entries;
754
+ try { entries = fs.readdirSync(skillsDir, { withFileTypes: true }); } catch { return; }
755
+ for (const dirent of entries) {
756
+ const entry = dirent.name;
757
+ if (entry === 'README.md') continue;
758
+ const entryPath = path.join(skillsDir, entry);
759
+ if (dirent.isDirectory()) {
760
+ const skillFile = path.join(entryPath, 'SKILL.md');
761
+ const nestedSkillFile = path.join(entryPath, 'skills', 'SKILL.md');
762
+ if (fs.existsSync(skillFile) && !seenSet.has(entry)) {
763
+ out.push({ file: 'SKILL.md', dir: entryPath, scope, skillName: entry, ...extra });
764
+ seenSet.add(entry);
765
+ } else if (fs.existsSync(nestedSkillFile) && !seenSet.has(entry)) {
766
+ out.push({ file: 'SKILL.md', dir: path.join(entryPath, 'skills'), scope, skillName: entry, ...extra });
767
+ seenSet.add(entry);
768
+ }
769
+ } else if (entry.endsWith('.md') && scope === 'project') {
770
+ const key = extra.projectName ? `${extra.projectName}:${entry}` : entry;
771
+ if (seenSet.has(key)) continue;
772
+ out.push({ file: entry, dir: skillsDir, scope, ...extra });
773
+ seenSet.add(key);
774
+ }
775
+ }
776
+ }
777
+
778
+ let _skillsCache = null;
779
+ let _skillsCacheTs = 0;
780
+ let _skillsCacheKey = null;
781
+ let _skillIndexCache = null;
782
+ let _skillIndexCacheTs = 0;
783
+ let _skillIndexCacheKey = null;
784
+ const SKILLS_CACHE_TTL = 30000; // 30s — skill files change rarely (agent extraction, manual authoring)
785
+
786
+ function invalidateSkillsCache() {
787
+ _skillsCache = null;
788
+ _skillsCacheTs = 0;
789
+ _skillsCacheKey = null;
790
+ _skillIndexCache = null;
791
+ _skillIndexCacheTs = 0;
792
+ _skillIndexCacheKey = null;
793
+ }
794
+
795
+ function _skillsCacheKeyFor(config, homeDir) {
796
+ const projects = getProjects(config).map(p => [p.name || '', p.localPath || '']);
797
+ return JSON.stringify({ homeDir, projects });
798
+ }
799
+
725
800
  function collectSkillFiles(config) {
801
+ const now = Date.now();
726
802
  config = config || getConfig();
803
+ const homeDir = os.homedir();
804
+ const projects = getProjects(config);
805
+ const cacheKey = _skillsCacheKeyFor(config, homeDir);
806
+ if (_skillsCache && _skillsCacheKey === cacheKey && (now - _skillsCacheTs) < SKILLS_CACHE_TTL) return _skillsCache;
727
807
  const skillFiles = [];
728
808
  const seen = new Set(); // dedup by name
729
809
 
730
- // 1. Claude Code native skills: ~/.claude/skills/<name>/SKILL.md
731
- const homeDir = os.homedir();
732
- const claudeSkillsDir = path.join(homeDir, '.claude', 'skills');
810
+ // 1. Runtime-native skills. Runtime adapters own their native locations so
811
+ // Minions stays a thin orchestration layer rather than a parallel skills system.
812
+ const seenByScope = new Map();
813
+ function seenFor(scope, projectName) {
814
+ const key = scope === 'project' ? `project:${projectName || ''}` : scope;
815
+ if (!seenByScope.has(key)) seenByScope.set(key, new Set());
816
+ return seenByScope.get(key);
817
+ }
733
818
  try {
734
- const dirs = fs.readdirSync(claudeSkillsDir).filter(d => {
735
- try { return fs.statSync(path.join(claudeSkillsDir, d)).isDirectory(); } catch { return false; }
736
- });
737
- for (const d of dirs) {
738
- // Check both <name>/SKILL.md and <name>/skills/SKILL.md (Claude Code uses both)
739
- const skillFile = path.join(claudeSkillsDir, d, 'SKILL.md');
740
- const nestedSkillFile = path.join(claudeSkillsDir, d, 'skills', 'SKILL.md');
741
- if (fs.existsSync(skillFile)) {
742
- skillFiles.push({ file: 'SKILL.md', dir: path.join(claudeSkillsDir, d), scope: 'claude-code', skillName: d });
743
- seen.add(d);
744
- } else if (fs.existsSync(nestedSkillFile)) {
745
- skillFiles.push({ file: 'SKILL.md', dir: path.join(claudeSkillsDir, d, 'skills'), scope: 'claude-code', skillName: d });
746
- seen.add(d);
819
+ const { listRuntimes, resolveRuntime } = require('./runtimes');
820
+ for (const runtimeName of listRuntimes()) {
821
+ const runtime = resolveRuntime(runtimeName);
822
+ if (typeof runtime.getSkillRoots !== 'function') continue;
823
+ for (const root of runtime.getSkillRoots({ homeDir })) {
824
+ _collectNativeSkillsDir(root.dir, root.scope, seenFor(root.scope, root.projectName), skillFiles, {
825
+ projectName: root.projectName,
826
+ });
827
+ }
828
+ for (const project of projects) {
829
+ if (!project.localPath) continue;
830
+ for (const root of runtime.getSkillRoots({ homeDir, project })) {
831
+ if (root.scope !== 'project') continue;
832
+ _collectNativeSkillsDir(root.dir, root.scope, seenFor(root.scope, root.projectName), skillFiles, {
833
+ projectName: root.projectName,
834
+ });
835
+ }
747
836
  }
748
837
  }
749
- } catch { /* optional */ }
838
+ } catch { /* runtime registry optional in partial installs */ }
750
839
 
751
840
  // 1b. Installed plugin skills: ~/.claude/plugins/installed_plugins.json
752
841
  // Plugins use commands/*.md and/or skills/<name>/SKILL.md and/or skills/SKILL.md
@@ -772,60 +861,42 @@ function collectSkillFiles(config) {
772
861
  } catch { /* optional */ }
773
862
 
774
863
  // skills/<name>/SKILL.md or skills/SKILL.md (newer style)
775
- const skillsDir = path.join(install.installPath, 'skills');
776
- try {
777
- const entries = fs.readdirSync(skillsDir);
778
- for (const entry of entries) {
779
- const entryPath = path.join(skillsDir, entry);
780
- if (entry === 'SKILL.md') {
781
- // Flat: skills/SKILL.md
782
- const name = pluginName;
783
- if (!seen.has(name)) {
784
- skillFiles.push({ file: 'SKILL.md', dir: skillsDir, scope: 'plugin', skillName: name });
785
- seen.add(name);
786
- }
787
- } else {
788
- try {
789
- if (!fs.statSync(entryPath).isDirectory()) continue;
790
- } catch { continue; }
791
- // Nested: skills/<name>/SKILL.md
792
- const nestedSkill = path.join(entryPath, 'SKILL.md');
793
- if (fs.existsSync(nestedSkill)) {
794
- const name = pluginName + ':' + entry;
795
- if (!seen.has(name)) {
796
- skillFiles.push({ file: 'SKILL.md', dir: entryPath, scope: 'plugin', skillName: name });
797
- seen.add(name);
798
- }
799
- }
800
- }
801
- }
802
- } catch { /* optional */ }
864
+ _collectPluginSkillsDir(path.join(install.installPath, 'skills'), pluginName, 'plugin', seen, skillFiles);
803
865
  }
804
866
  } catch { /* optional */ }
805
867
 
806
- // 2. Project-specific skills: <project>/.claude/skills/<name>.md or <name>/SKILL.md
807
- for (const project of getProjects(config)) {
808
- const projectSkillsDir = path.resolve(project.localPath, '.claude', 'skills');
809
- try {
810
- const entries = fs.readdirSync(projectSkillsDir);
811
- for (const entry of entries) {
812
- if (entry === 'README.md') continue;
813
- const entryPath = path.join(projectSkillsDir, entry);
814
- const stat = fs.statSync(entryPath);
815
- if (stat.isDirectory()) {
816
- const skillFile = path.join(entryPath, 'SKILL.md');
817
- if (fs.existsSync(skillFile)) {
818
- skillFiles.push({ file: 'SKILL.md', dir: entryPath, scope: 'project', projectName: project.name, skillName: entry });
819
- }
820
- } else if (entry.endsWith('.md')) {
821
- skillFiles.push({ file: entry, dir: projectSkillsDir, scope: 'project', projectName: project.name });
822
- }
868
+ // 1c. Copilot installed plugin skills:
869
+ // ~/.copilot/installed-plugins/<source>/<plugin>/skills/<skill>/SKILL.md
870
+ // Separate dedup set so plugins installed in both engines surface in both tabs.
871
+ const copilotSeen = new Set();
872
+ try {
873
+ const sources = fs.readdirSync(path.join(homeDir, '.copilot', 'installed-plugins'), { withFileTypes: true });
874
+ for (const sourceDirent of sources) {
875
+ if (!sourceDirent.isDirectory()) continue;
876
+ const sourceDir = path.join(homeDir, '.copilot', 'installed-plugins', sourceDirent.name);
877
+ let plugins;
878
+ try { plugins = fs.readdirSync(sourceDir, { withFileTypes: true }); } catch { continue; }
879
+ for (const pluginDirent of plugins) {
880
+ if (!pluginDirent.isDirectory()) continue;
881
+ _collectPluginSkillsDir(path.join(sourceDir, pluginDirent.name, 'skills'), pluginDirent.name, 'copilot-plugin', copilotSeen, skillFiles);
823
882
  }
824
- } catch { /* optional */ }
825
- }
883
+ }
884
+ } catch { /* optional */ }
885
+
886
+ _skillsCache = skillFiles;
887
+ _skillsCacheTs = now;
888
+ _skillsCacheKey = cacheKey;
826
889
  return skillFiles;
827
890
  }
828
891
 
892
+ const SKILL_SOURCE_BY_SCOPE = {
893
+ 'claude-code': 'claude-code',
894
+ 'copilot': 'copilot',
895
+ 'agent-skill': 'agent-skill',
896
+ 'plugin': 'plugin',
897
+ 'copilot-plugin': 'copilot-plugin',
898
+ };
899
+
829
900
  function getSkills(config) {
830
901
  const all = [];
831
902
  for (const { file: f, dir, scope, projectName, skillName } of collectSkillFiles(config)) {
@@ -835,9 +906,10 @@ function getSkills(config) {
835
906
  if (scope === 'project' && meta.project === 'any') meta.project = projectName;
836
907
  // Check if auto-generated by an agent
837
908
  const isAutoGenerated = content.includes('Auto-extracted') || content.includes('author:') || content.includes('createdBy:');
909
+ const source = SKILL_SOURCE_BY_SCOPE[scope] || (scope === 'project' ? 'project:' + projectName : 'minions');
838
910
  all.push({
839
911
  ...meta, file: f, dir: dir.replace(/\\/g, '/'),
840
- source: scope === 'claude-code' ? 'claude-code' : scope === 'plugin' ? 'plugin' : scope === 'project' ? 'project:' + projectName : 'minions',
912
+ source,
841
913
  scope,
842
914
  autoGenerated: isAutoGenerated,
843
915
  });
@@ -847,8 +919,17 @@ function getSkills(config) {
847
919
  }
848
920
 
849
921
  function getSkillIndex(config) {
922
+ const now = Date.now();
923
+ config = config || getConfig();
924
+ const cacheKey = _skillsCacheKeyFor(config, os.homedir());
925
+ if (_skillIndexCache !== null && _skillIndexCacheKey === cacheKey && (now - _skillIndexCacheTs) < SKILLS_CACHE_TTL) return _skillIndexCache;
850
926
  try {
851
- const skillFiles = collectSkillFiles(config);
927
+ const skillFiles = collectSkillFiles(config).sort((a, b) => {
928
+ const priority = { project: 0, plugin: 1, 'copilot-plugin': 1, copilot: 2, 'agent-skill': 2, 'claude-code': 2 };
929
+ return (priority[a.scope] ?? 9) - (priority[b.scope] ?? 9)
930
+ || String(a.projectName || '').localeCompare(String(b.projectName || ''))
931
+ || String(a.skillName || a.file || '').localeCompare(String(b.skillName || b.file || ''));
932
+ });
852
933
  if (skillFiles.length === 0) return '';
853
934
 
854
935
  let index = '## Available Minions Skills\n\n';
@@ -866,10 +947,113 @@ function getSkillIndex(config) {
866
947
  index += `**File:** \`${dir}/${f}\`\n`;
867
948
  index += `Read the full skill file before following the steps.\n\n`;
868
949
  }
950
+ _skillIndexCache = index;
951
+ _skillIndexCacheTs = now;
952
+ _skillIndexCacheKey = cacheKey;
869
953
  return index;
870
954
  } catch { return ''; }
871
955
  }
872
956
 
957
+ // ── Claude/Copilot command docs ──────────────────────────────────────────────
958
+
959
+ function _collectMarkdownFilesRecursive(rootDir, maxFiles = 100) {
960
+ const found = [];
961
+ function walk(dir, relPrefix = '') {
962
+ if (found.length >= maxFiles) return;
963
+ let entries;
964
+ try { entries = fs.readdirSync(dir); } catch { return; }
965
+ for (const entry of entries) {
966
+ if (found.length >= maxFiles) return;
967
+ if (entry === 'README.md') continue;
968
+ const full = path.join(dir, entry);
969
+ let stat;
970
+ try { stat = fs.statSync(full); } catch { continue; }
971
+ if (stat.isDirectory()) {
972
+ walk(full, path.join(relPrefix, entry));
973
+ } else if (entry.endsWith('.md')) {
974
+ found.push({ file: entry, dir, rel: path.join(relPrefix, entry).replace(/\\/g, '/') });
975
+ }
976
+ }
977
+ }
978
+ walk(rootDir);
979
+ return found;
980
+ }
981
+
982
+ function collectCommandFiles(config) {
983
+ config = config || getConfig();
984
+ const commandFiles = [];
985
+ const seen = new Set();
986
+ const homeDir = os.homedir();
987
+
988
+ function addCommandDir(rootDir, scope, extra = {}) {
989
+ const root = path.resolve(rootDir);
990
+ for (const cmd of _collectMarkdownFilesRecursive(root)) {
991
+ const key = `${scope}:${extra.projectName || ''}:${root}:${cmd.rel}`;
992
+ if (seen.has(key)) continue;
993
+ seen.add(key);
994
+ const commandName = cmd.rel.replace(/\.md$/, '').replace(/\\/g, '/');
995
+ commandFiles.push({ ...cmd, root, scope, commandName, ...extra });
996
+ }
997
+ }
998
+
999
+ addCommandDir(path.join(homeDir, '.claude', 'commands'), 'claude-code');
1000
+
1001
+ try {
1002
+ const pluginsFile = path.join(homeDir, '.claude', 'plugins', 'installed_plugins.json');
1003
+ const registry = JSON.parse(safeRead(pluginsFile) || '{}');
1004
+ for (const [pluginKey, installs] of Object.entries(registry.plugins || {})) {
1005
+ if (!Array.isArray(installs) || installs.length === 0) continue;
1006
+ const install = installs[0];
1007
+ if (!install.installPath) continue;
1008
+ const pluginName = pluginKey.split('@')[0];
1009
+ addCommandDir(path.join(install.installPath, 'commands'), 'plugin', { pluginName });
1010
+ }
1011
+ } catch { /* optional */ }
1012
+
1013
+ for (const project of getProjects(config)) {
1014
+ if (!project.localPath) continue;
1015
+ addCommandDir(path.resolve(project.localPath, '.claude', 'commands'), 'project', { projectName: project.name });
1016
+ }
1017
+
1018
+ return commandFiles;
1019
+ }
1020
+
1021
+ function _commandTitle(content, fallback) {
1022
+ const description = content.match(/^description:\s*["']?(.+?)["']?\s*$/m);
1023
+ if (description) return description[1].trim();
1024
+ const heading = content.match(/^#\s+(.+)/m);
1025
+ if (heading) return heading[1].trim();
1026
+ return fallback;
1027
+ }
1028
+
1029
+ function getCommandIndex(config) {
1030
+ try {
1031
+ const commandFiles = collectCommandFiles(config).sort((a, b) => {
1032
+ const priority = { project: 0, plugin: 1, 'claude-code': 2 };
1033
+ return (priority[a.scope] ?? 9) - (priority[b.scope] ?? 9)
1034
+ || String(a.projectName || '').localeCompare(String(b.projectName || ''))
1035
+ || String(a.commandName || '').localeCompare(String(b.commandName || ''));
1036
+ });
1037
+ if (commandFiles.length === 0) return '';
1038
+
1039
+ let index = '## Available User Commands\n\n';
1040
+ index += 'Claude/Copilot command markdown discovered from user, plugin, and project command packs. Read the file and adapt its workflow when it matches the task; do not assume slash-command invocation works inside non-interactive agent runs.\n\n';
1041
+
1042
+ for (const { file: f, dir, scope, projectName, pluginName, commandName } of commandFiles) {
1043
+ const content = safeRead(path.join(dir, f)) || '';
1044
+ const title = _commandTitle(content, commandName);
1045
+ const label = scope === 'project'
1046
+ ? `project:${projectName || 'unknown'}`
1047
+ : scope === 'plugin'
1048
+ ? `plugin:${pluginName || 'unknown'}`
1049
+ : 'claude-code';
1050
+ index += `- \`/${commandName}\` (${label}) — ${title}\n`;
1051
+ index += ` File: \`${dir.replace(/\\/g, '/')}/${f}\`\n`;
1052
+ }
1053
+ return index + '\n';
1054
+ } catch { return ''; }
1055
+ }
1056
+
873
1057
  // ── Knowledge Base ──────────────────────────────────────────────────────────
874
1058
 
875
1059
  let _kbCache = null;
@@ -1370,7 +1554,10 @@ module.exports = {
1370
1554
  getPrs, getPullRequests,
1371
1555
 
1372
1556
  // Skills
1373
- collectSkillFiles, getSkills, getSkillIndex,
1557
+ collectSkillFiles, getSkills, getSkillIndex, invalidateSkillsCache,
1558
+
1559
+ // Commands
1560
+ collectCommandFiles, getCommandIndex,
1374
1561
 
1375
1562
  // Knowledge base
1376
1563
  getKnowledgeBaseEntries, getKnowledgeBaseIndex,
@@ -322,6 +322,29 @@ function buildPrompt(promptText, /* sysPromptText */ _sys) {
322
322
  return String(promptText == null ? '' : promptText);
323
323
  }
324
324
 
325
+ function getUserAssetDirs({ homeDir = os.homedir() } = {}) {
326
+ return [path.join(homeDir, '.claude')];
327
+ }
328
+
329
+ function getSkillRoots({ homeDir = os.homedir(), project = null } = {}) {
330
+ const roots = [{ dir: path.join(homeDir, '.claude', 'skills'), scope: 'claude-code' }];
331
+ if (project?.localPath) {
332
+ roots.push({
333
+ dir: path.resolve(project.localPath, '.claude', 'skills'),
334
+ scope: 'project',
335
+ projectName: project.name,
336
+ });
337
+ }
338
+ return roots;
339
+ }
340
+
341
+ function getSkillWriteTargets({ homeDir = os.homedir(), project = null } = {}) {
342
+ return {
343
+ personal: path.join(homeDir, '.claude', 'skills'),
344
+ project: project?.localPath ? path.resolve(project.localPath, '.claude', 'skills') : null,
345
+ };
346
+ }
347
+
325
348
  // ── Output Parsing ───────────────────────────────────────────────────────────
326
349
 
327
350
  /**
@@ -612,6 +635,32 @@ const capabilities = {
612
635
  // (fatal error message). Multi-line so all platforms see actionable guidance.
613
636
  const INSTALL_HINT = 'install from https://claude.ai/download or: npm install -g @anthropic-ai/claude-code';
614
637
 
638
+ function getUserAssetDirs({ homeDir = os.homedir() } = {}) {
639
+ return [path.join(homeDir, '.claude')];
640
+ }
641
+
642
+ function getSkillRoots({ homeDir = os.homedir(), project = null } = {}) {
643
+ const roots = [
644
+ { dir: path.join(homeDir, '.claude', 'skills'), scope: 'claude-code' },
645
+ ];
646
+ if (project?.localPath) {
647
+ roots.push({
648
+ dir: path.join(project.localPath, '.claude', 'skills'),
649
+ scope: 'project',
650
+ projectName: project.name || path.basename(project.localPath),
651
+ });
652
+ }
653
+ return roots;
654
+ }
655
+
656
+ function getSkillWriteTargets({ homeDir = os.homedir(), project = null } = {}) {
657
+ const targets = { personal: path.join(homeDir, '.claude', 'skills') };
658
+ if (project?.localPath) {
659
+ targets.project = path.join(project.localPath, '.claude', 'skills');
660
+ }
661
+ return targets;
662
+ }
663
+
615
664
  module.exports = {
616
665
  name: 'claude',
617
666
  capabilities,
@@ -624,6 +673,9 @@ module.exports = {
624
673
  buildSpawnFlags,
625
674
  buildArgs,
626
675
  buildPrompt,
676
+ getUserAssetDirs,
677
+ getSkillRoots,
678
+ getSkillWriteTargets,
627
679
  getResumeSessionId,
628
680
  saveSession,
629
681
  detectPermissionGate,
@@ -29,6 +29,7 @@
29
29
 
30
30
  const fs = require('fs');
31
31
  const https = require('https');
32
+ const os = require('os');
32
33
  const path = require('path');
33
34
  const { execSync } = require('child_process');
34
35
  const { FAILURE_CLASS, safeWrite, ts } = require('../shared');
@@ -348,6 +349,41 @@ function buildPrompt(promptText, sysPromptText, opts = {}) {
348
349
  return `<system>\n${String(sysPromptText)}\n</system>\n\n${user}`;
349
350
  }
350
351
 
352
+ function getUserAssetDirs({ homeDir = os.homedir() } = {}) {
353
+ return [
354
+ path.join(homeDir, '.copilot'),
355
+ path.join(homeDir, '.agents'),
356
+ ];
357
+ }
358
+
359
+ function getSkillRoots({ homeDir = os.homedir(), project = null } = {}) {
360
+ const roots = [
361
+ { dir: path.join(homeDir, '.copilot', 'skills'), scope: 'copilot' },
362
+ { dir: path.join(homeDir, '.agents', 'skills'), scope: 'agent-skill' },
363
+ ];
364
+ if (project?.localPath) {
365
+ for (const rel of [
366
+ ['.github', 'skills'],
367
+ ['.claude', 'skills'],
368
+ ['.agents', 'skills'],
369
+ ]) {
370
+ roots.push({
371
+ dir: path.resolve(project.localPath, ...rel),
372
+ scope: 'project',
373
+ projectName: project.name,
374
+ });
375
+ }
376
+ }
377
+ return roots;
378
+ }
379
+
380
+ function getSkillWriteTargets({ homeDir = os.homedir(), project = null } = {}) {
381
+ return {
382
+ personal: path.join(homeDir, '.copilot', 'skills'),
383
+ project: project?.localPath ? path.resolve(project.localPath, '.github', 'skills') : null,
384
+ };
385
+ }
386
+
351
387
  // ── Output Parsing ──────────────────────────────────────────────────────────
352
388
  //
353
389
  // Whitelist of event types observed during the spike (docs/copilot-cli-schema.md
@@ -754,6 +790,36 @@ const capabilities = {
754
790
  // - Direct: download from https://github.com/github/copilot-cli/releases
755
791
  const INSTALL_HINT = 'install via WinGet (winget install --id GitHub.cli && gh extension install github/gh-copilot), Homebrew (brew install gh && gh extension install github/gh-copilot), or download standalone copilot from https://github.com/github/copilot-cli/releases';
756
792
 
793
+ function getUserAssetDirs({ homeDir = os.homedir() } = {}) {
794
+ return [
795
+ path.join(homeDir, '.copilot'),
796
+ path.join(homeDir, '.agents'),
797
+ ];
798
+ }
799
+
800
+ function getSkillRoots({ homeDir = os.homedir(), project = null } = {}) {
801
+ const roots = [
802
+ { dir: path.join(homeDir, '.copilot', 'skills'), scope: 'copilot' },
803
+ { dir: path.join(homeDir, '.agents', 'skills'), scope: 'agent-skill' },
804
+ ];
805
+ if (project?.localPath) {
806
+ const projectName = project.name || path.basename(project.localPath);
807
+ roots.push(
808
+ { dir: path.join(project.localPath, '.github', 'skills'), scope: 'project', projectName },
809
+ { dir: path.join(project.localPath, '.agents', 'skills'), scope: 'project', projectName },
810
+ );
811
+ }
812
+ return roots;
813
+ }
814
+
815
+ function getSkillWriteTargets({ homeDir = os.homedir(), project = null } = {}) {
816
+ const targets = { personal: path.join(homeDir, '.copilot', 'skills') };
817
+ if (project?.localPath) {
818
+ targets.project = path.join(project.localPath, '.github', 'skills');
819
+ }
820
+ return targets;
821
+ }
822
+
757
823
  module.exports = {
758
824
  name: 'copilot',
759
825
  capabilities,
@@ -767,6 +833,9 @@ module.exports = {
767
833
  buildSpawnFlags,
768
834
  buildArgs,
769
835
  buildPrompt,
836
+ getUserAssetDirs,
837
+ getSkillRoots,
838
+ getSkillWriteTargets,
770
839
  getResumeSessionId,
771
840
  saveSession,
772
841
  detectPermissionGate,
package/engine/shared.js CHANGED
@@ -212,6 +212,33 @@ function safeUnlink(p) {
212
212
  try { fs.unlinkSync(p); } catch { /* cleanup */ }
213
213
  }
214
214
 
215
+ function neutralizeJsonBackupSidecar(filePath, inertData = { status: 'archived' }) {
216
+ const backupPath = filePath + '.backup';
217
+ try {
218
+ fs.unlinkSync(backupPath);
219
+ return { ok: true, action: 'removed', backupPath };
220
+ } catch (unlinkErr) {
221
+ if (unlinkErr.code === 'ENOENT') return { ok: true, action: 'absent', backupPath };
222
+ try {
223
+ safeWrite(backupPath, inertData);
224
+ return {
225
+ ok: true,
226
+ action: 'neutralized',
227
+ backupPath,
228
+ unlinkError: unlinkErr.message,
229
+ };
230
+ } catch (writeErr) {
231
+ return {
232
+ ok: false,
233
+ action: 'failed',
234
+ backupPath,
235
+ unlinkError: unlinkErr.message,
236
+ writeError: writeErr.message,
237
+ };
238
+ }
239
+ }
240
+ }
241
+
215
242
  // ── Dispatch Prompt Sidecar (#1167) ─────────────────────────────────────────
216
243
  // Large prompts (PR diffs, build error logs, coalesced human feedback) inlined
217
244
  // into dispatch.json caused hundreds-of-MB bloat per entry and eventual V8 OOM
@@ -2387,6 +2414,7 @@ module.exports = {
2387
2414
  safeJson, safeJsonObj, safeJsonArr,
2388
2415
  safeWrite,
2389
2416
  safeUnlink,
2417
+ neutralizeJsonBackupSidecar,
2390
2418
  PROMPT_CONTEXTS_DIR,
2391
2419
  dispatchPromptSidecarPath,
2392
2420
  dispatchCompletionReportPath,
@@ -174,15 +174,18 @@ function main() {
174
174
  opts.sysPromptFile = sysTmpPath;
175
175
  }
176
176
 
177
- // Skill discovery dirs — agents run with CWD set to an external repo
178
- // worktree, so skills in the minions repo and the user's global ~/.claude
179
- // dir are otherwise invisible. The adapter decides how to surface them
180
- // (Claude → `--add-dir <path>`; Copilot → ignored).
177
+ // User asset discovery dirs — agents run with CWD set to an external repo
178
+ // worktree, so the adapter supplies any runtime-native global asset roots
179
+ // that should be visible from that cwd.
181
180
  const minionsDir = path.resolve(__dirname, '..');
182
- const userClaudeDir = path.join(os.homedir(), '.claude');
181
+ const runtimeAssetDirs = typeof runtime.getUserAssetDirs === 'function'
182
+ ? runtime.getUserAssetDirs({ homeDir: os.homedir() })
183
+ : [];
183
184
  const addDirs = [minionsDir];
184
- if (fs.existsSync(userClaudeDir) && path.resolve(userClaudeDir) !== path.resolve(minionsDir)) {
185
- addDirs.push(userClaudeDir);
185
+ for (const userAssetDir of runtimeAssetDirs) {
186
+ if (fs.existsSync(userAssetDir) && path.resolve(userAssetDir) !== path.resolve(minionsDir)) {
187
+ addDirs.push(userAssetDir);
188
+ }
186
189
  }
187
190
 
188
191
  let resolved;