@yemi33/minions 0.1.1681 → 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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1682 (2026-05-02)
4
+
5
+ ### Features
6
+ - index Copilot plugin skills and surface as separate dashboard tab
7
+
8
+ ### Other
9
+ - test(meeting): add direct unit tests for internal scoring/formatting/path-resolution helpers (#1985)
10
+ - docs: document new optional runtime adapter methods for skills
11
+ - Keep runtime skills adapter-owned
12
+ - docs: update copilot instructions
13
+
3
14
  ## 0.1.1681 (2026-05-02)
4
15
 
5
16
  ### Features
@@ -13,13 +13,28 @@ function renderSkills(skills) {
13
13
  return;
14
14
  }
15
15
 
16
- const sourceIcon = (s) => s === 'claude-code' ? '⚡' : s === 'plugin' ? '🔌' : s?.startsWith('project:') ? '📁' : '🔧';
17
- const sourceLabel = (s) => s === 'plugin' ? 'plugin' : s?.startsWith('project:') ? s.replace('project:', '') : 'global';
16
+ const SOURCE_META = {
17
+ 'claude-code': { icon: '', label: 'global', group: 'global' },
18
+ 'copilot': { icon: '🤖', label: 'copilot', group: 'copilot' },
19
+ 'agent-skill': { icon: '🤝', label: 'agent', group: 'agent' },
20
+ 'plugin': { icon: '🔌', label: 'plugin', group: 'plugins' },
21
+ 'copilot-plugin': { icon: '🤖', label: 'copilot', group: 'copilot' },
22
+ };
23
+ const metaOf = (s) => {
24
+ if (SOURCE_META[s]) return SOURCE_META[s];
25
+ if (s && s.startsWith('project:')) {
26
+ const name = s.slice('project:'.length);
27
+ return { icon: '📁', label: name, group: name };
28
+ }
29
+ return { icon: '🔧', label: 'global', group: 'global' };
30
+ };
31
+ const sourceIcon = (s) => metaOf(s).icon;
32
+ const sourceLabel = (s) => metaOf(s).label;
18
33
 
19
34
  // Group by source
20
35
  const groups = {};
21
36
  for (const r of skills) {
22
- const key = r.source === 'plugin' ? 'plugins' : r.source?.startsWith('project:') ? r.source.replace('project:', '') : 'global';
37
+ const key = metaOf(r.source).group;
23
38
  if (!groups[key]) groups[key] = [];
24
39
  groups[key].push(r);
25
40
  }
package/dashboard.js CHANGED
@@ -170,10 +170,20 @@ function _normalizeSkillDirForCompare(dir) {
170
170
  return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
171
171
  }
172
172
 
173
+ // Mirrors the source string emitted by `getSkills()` so `_resolveSkillReadPath`
174
+ // can match a request's `source` param against the entries collectSkillFiles returns.
175
+ const _SKILL_ENTRY_SOURCE_BY_SCOPE = {
176
+ 'claude-code': 'claude-code',
177
+ 'copilot': 'copilot',
178
+ 'agent-skill': 'agent-skill',
179
+ 'plugin': 'plugin',
180
+ 'copilot-plugin': 'copilot-plugin',
181
+ };
182
+
173
183
  function _skillEntrySource(entry) {
174
184
  if (!entry) return '';
175
- if (entry.scope === 'claude-code') return 'claude-code';
176
- if (entry.scope === 'plugin') return 'plugin';
185
+ const mapped = _SKILL_ENTRY_SOURCE_BY_SCOPE[entry.scope];
186
+ if (mapped) return mapped;
177
187
  if (entry.scope === 'project') return 'project:' + entry.projectName;
178
188
  return entry.scope || '';
179
189
  }
@@ -4495,17 +4505,35 @@ What would you like to discuss or change? When you're happy, say "approve" and I
4495
4505
  let content = '';
4496
4506
  const skillPath = _resolveSkillReadPath({ file, dir, source, config: CONFIG });
4497
4507
  if (skillPath) content = safeRead(skillPath) || '';
4508
+ // Fallback when caller didn't supply `dir`: try the source's known native
4509
+ // locations. `_resolveSkillReadPath` only matches entries returned by
4510
+ // `collectSkillFiles`, so a skill that already has `dir` will resolve there.
4498
4511
  if (!content && !dir) {
4499
- // Fallback: search Claude Code skills, then project skills
4500
4512
  const home = os.homedir();
4501
- const claudePath = path.join(home, '.claude', 'skills', file.replace('.md', '').replace('SKILL', ''), 'SKILL.md');
4502
- content = safeRead(claudePath) || '';
4503
- if (!content) {
4504
- if (source.startsWith('project:')) {
4505
- const proj = PROJECTS.find(p => p.name === source.replace('project:', ''));
4506
- if (proj) content = safeRead(path.join(proj.localPath, '.claude', 'skills', file)) || '';
4513
+ const skillStem = file.replace(/\.md$/, '').replace(/^SKILL$/, '');
4514
+ const candidates = [];
4515
+ if (source === 'claude-code' || !source) {
4516
+ candidates.push(path.join(home, '.claude', 'skills', skillStem, 'SKILL.md'));
4517
+ }
4518
+ if (source === 'copilot') {
4519
+ candidates.push(path.join(home, '.copilot', 'skills', skillStem, 'SKILL.md'));
4520
+ }
4521
+ if (source === 'agent-skill') {
4522
+ candidates.push(path.join(home, '.agents', 'skills', skillStem, 'SKILL.md'));
4523
+ }
4524
+ if (source.startsWith('project:')) {
4525
+ const proj = PROJECTS.find(p => p.name === source.slice('project:'.length));
4526
+ if (proj?.localPath) {
4527
+ for (const sub of ['.claude', '.github', '.agents']) {
4528
+ candidates.push(path.join(proj.localPath, sub, 'skills', file));
4529
+ candidates.push(path.join(proj.localPath, sub, 'skills', skillStem, 'SKILL.md'));
4530
+ }
4507
4531
  }
4508
4532
  }
4533
+ for (const c of candidates) {
4534
+ content = safeRead(c) || '';
4535
+ if (content) break;
4536
+ }
4509
4537
  }
4510
4538
  res.setHeader('Content-Type', 'text/plain; charset=utf-8');
4511
4539
  res.setHeader('Access-Control-Allow-Origin', '*');
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-02T13:35:00.752Z"
4
+ "cachedAt": "2026-05-02T15:15:12.425Z"
5
5
  }
@@ -1626,9 +1626,23 @@ function checkForLearnings(agentId, agentInfo, taskDesc) {
1626
1626
  log('warn', `${agentInfo?.name || agentId} didn't write learnings — no follow-up queued`);
1627
1627
  }
1628
1628
 
1629
- function extractSkillsFromOutput(output, agentId, dispatchItem, config) {
1629
+ function skillWriteTargets(runtimeName, project = null) {
1630
+ try {
1631
+ const runtime = resolveRuntime(runtimeName || 'claude');
1632
+ if (typeof runtime.getSkillWriteTargets === 'function') {
1633
+ return runtime.getSkillWriteTargets({ homeDir: os.homedir(), project });
1634
+ }
1635
+ } catch { /* fall through to Claude-compatible legacy target */ }
1636
+ return {
1637
+ personal: path.join(os.homedir(), '.claude', 'skills'),
1638
+ project: project?.localPath ? path.resolve(project.localPath, '.claude', 'skills') : null,
1639
+ };
1640
+ }
1641
+
1642
+ function extractSkillsFromOutput(output, agentId, dispatchItem, config, runtimeName = null) {
1630
1643
 
1631
1644
  if (!output) return;
1645
+ const effectiveRuntime = runtimeName || dispatchItem?.meta?.runtimeName || dispatchItem?.runtimeName || 'claude';
1632
1646
  let fullText = '';
1633
1647
  for (const line of output.split('\n')) {
1634
1648
  try {
@@ -1666,6 +1680,9 @@ function extractSkillsFromOutput(output, agentId, dispatchItem, config) {
1666
1680
  if (scope === 'project' && project) {
1667
1681
  const proj = shared.getProjects(config).find(p => p.name === project);
1668
1682
  if (proj) {
1683
+ const projectSkillRoot = skillWriteTargets(effectiveRuntime, proj).project
1684
+ || path.resolve(proj.localPath, '.claude', 'skills');
1685
+ const projectSkillPath = path.join(projectSkillRoot, skillDirName, 'SKILL.md');
1669
1686
  const centralPath = path.join(MINIONS_DIR, 'work-items.json');
1670
1687
  let skillId = null;
1671
1688
  mutateJsonFileLocked(centralPath, data => {
@@ -1673,7 +1690,7 @@ function extractSkillsFromOutput(output, agentId, dispatchItem, config) {
1673
1690
  if (data.some(i => i.title === `Add skill: ${name}` && i.status !== WI_STATUS.FAILED)) return data;
1674
1691
  skillId = `SK${String(data.filter(i => i.id?.startsWith('SK')).length + 1).padStart(3, '0')}`;
1675
1692
  data.push({ id: skillId, type: 'implement', title: `Add skill: ${name}`,
1676
- description: `Create project-level skill \`${skillDirName}/SKILL.md\` in ${project}.\n\nWrite this file to \`${proj.localPath}/.claude/skills/${skillDirName}/SKILL.md\` via a PR.\n\n## Skill Content\n\n\`\`\`\n${enrichedBlock}\n\`\`\``,
1693
+ description: `Create project-level skill \`${skillDirName}/SKILL.md\` in ${project}.\n\nWrite this file to \`${projectSkillPath}\` via a PR.\n\n## Skill Content\n\n\`\`\`\n${enrichedBlock}\n\`\`\``,
1677
1694
  priority: 'low', status: WI_STATUS.QUEUED, created: ts(), createdBy: `engine:skill-extraction:${agentName}` });
1678
1695
  return data;
1679
1696
  }, { skipWriteIfUnchanged: true });
@@ -1682,18 +1699,18 @@ function extractSkillsFromOutput(output, agentId, dispatchItem, config) {
1682
1699
  }
1683
1700
  }
1684
1701
  } else {
1685
- // Write in Claude Code native format: ~/.claude/skills/<name>/SKILL.md
1686
- const claudeSkillsDir = path.join(os.homedir(), '.claude', 'skills');
1687
- const skillDir = path.join(claudeSkillsDir, name.replace(/[^a-z0-9-]/g, '-'));
1702
+ const personalSkillRoot = skillWriteTargets(effectiveRuntime).personal;
1703
+ const skillDir = path.join(personalSkillRoot, name.replace(/[^a-z0-9-]/g, '-'));
1688
1704
  const skillPath = path.join(skillDir, 'SKILL.md');
1689
1705
  if (!fs.existsSync(skillPath)) {
1690
- // Convert to Claude Code format: only name + description in frontmatter
1706
+ // Native skill format: only name + description in frontmatter.
1691
1707
  const description = m('description') || m('trigger') || `Auto-extracted skill from ${agentName}`;
1692
1708
  const body = fmMatch[2] || '';
1693
1709
  const ccContent = `---\nname: ${name}\ndescription: ${description}\n---\n\n${body.trim()}\n`;
1694
1710
  if (!fs.existsSync(skillDir)) fs.mkdirSync(skillDir, { recursive: true });
1695
1711
  shared.safeWrite(skillPath, ccContent);
1696
- log('info', `Extracted skill "${name}" from ${agentName} → ~/.claude/skills/${name.replace(/[^a-z0-9-]/g, '-')}/SKILL.md`);
1712
+ try { require('./queries').invalidateSkillsCache(); } catch {}
1713
+ log('info', `Extracted skill "${name}" from ${agentName} → ${skillPath}`);
1697
1714
  } else {
1698
1715
  log('info', `Skill "${name}" already exists, skipping`);
1699
1716
  }
package/engine/meeting.js CHANGED
@@ -45,7 +45,7 @@ function getStructuredNoteArtifacts(structuredCompletion) {
45
45
 
46
46
  function isPathInside(parent, child) {
47
47
  const rel = path.relative(parent, child);
48
- return rel && !rel.startsWith('..') && !path.isAbsolute(rel);
48
+ return Boolean(rel && !rel.startsWith('..') && !path.isAbsolute(rel));
49
49
  }
50
50
 
51
51
  function resolveMeetingNoteArtifactPath(artifactPath) {
@@ -114,27 +114,34 @@ function formatMeetingContributions(entries, agents, emptyText, label, maxBytes)
114
114
  return truncateMeetingContext(combined, maxBytes, label);
115
115
  }
116
116
 
117
- function cleanMeetingSummaryText(text) {
117
+ function stripMeetingSummaryMarkdown(text) {
118
118
  return String(text || '')
119
119
  .replace(/\r/g, '')
120
- .replace(/```[\s\S]*?```/g, ' ')
120
+ .replace(/```[\s\S]*?```/g, '\n')
121
121
  .replace(/`([^`]+)`/g, '$1')
122
122
  .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
123
- .replace(/^[#>*-]+\s*/gm, '')
123
+ .replace(/^\s*(?:[#>*-]+|\d+[.)])\s*/gm, '');
124
+ }
125
+
126
+ function cleanMeetingSummaryText(text) {
127
+ return stripMeetingSummaryMarkdown(text)
124
128
  .replace(/\s+/g, ' ')
125
129
  .trim();
126
130
  }
127
131
 
128
132
  function splitMeetingSummaryFragments(text) {
129
- return cleanMeetingSummaryText(text)
130
- .split(/\n+|(?:[.!?])\s+|;\s+/)
131
- .map(s => s.trim())
133
+ return stripMeetingSummaryMarkdown(text)
134
+ .split(/\n+|[.!?]+\s*|;\s*/)
135
+ .map(s => s.replace(/\s+/g, ' ').trim())
132
136
  .filter(Boolean);
133
137
  }
134
138
 
135
139
  function truncateMeetingSummary(text, maxLen) {
136
- if (text.length <= maxLen) return text;
137
- return text.slice(0, Math.max(0, maxLen - 1)).trimEnd() + '';
140
+ const value = String(text || '');
141
+ if (!value) return '';
142
+ if (!Number.isFinite(maxLen) || maxLen <= 0) return '';
143
+ if (value.length < maxLen) return value;
144
+ return value.slice(0, Math.max(0, maxLen - 1)) + '…';
138
145
  }
139
146
 
140
147
  function formatMeetingSummaryBullets(entries, agents, emptyText, maxLen) {
@@ -148,13 +155,14 @@ function formatMeetingSummaryBullets(entries, agents, emptyText, maxLen) {
148
155
  }
149
156
 
150
157
  function scoreMeetingTakeaway(fragment) {
151
- const lower = fragment.toLowerCase();
158
+ const value = String(fragment || '');
159
+ const lower = value.toLowerCase();
152
160
  let score = 0;
153
161
  if (/(should|must|need to|needs to|recommend|recommended|action|next step|follow up|fix|mitigat|investigat|verify|test|block)/.test(lower)) score += 4;
154
162
  if (/(agree|aligned|consensus|support|prefer)/.test(lower)) score += 3;
155
163
  if (/(disagree|however|but|risk|risky|concern|trade-off|question|uncertain|worry)/.test(lower)) score += 3;
156
- if (fragment.length >= 40 && fragment.length <= 180) score += 2;
157
- if (fragment.length > 220) score -= 1;
164
+ if (value.length >= 40 && value.length <= 180) score += 2;
165
+ if (value.length > 220) score -= 1;
158
166
  return score;
159
167
  }
160
168
 
@@ -195,7 +203,9 @@ function collectMeetingNextSteps(meeting) {
195
203
  }
196
204
  }
197
205
  }
198
- return ['- Review the findings and debate, then add a human-written conclusion if more nuance is needed.'];
206
+ return steps.length
207
+ ? steps
208
+ : ['- Review the findings and debate, then add a human-written conclusion if more nuance is needed.'];
199
209
  }
200
210
 
201
211
  function buildTimedOutMeetingConclusion(meeting, agents) {
@@ -615,4 +625,17 @@ module.exports = {
615
625
  discoverMeetingWork, collectMeetingFindings, checkMeetingTimeouts,
616
626
  addMeetingNote, advanceMeetingRound, endMeeting, archiveMeeting, unarchiveMeeting, deleteMeeting,
617
627
  EMPTY_OUTPUT_PATTERNS,
628
+ // exported for testing — engine code MUST go through
629
+ // getMeetings/discoverMeetingWork/collectMeetingFindings/checkMeetingTimeouts,
630
+ // never these helpers directly.
631
+ isPathInside,
632
+ resolveMeetingNoteArtifactPath,
633
+ cleanMeetingSummaryText,
634
+ splitMeetingSummaryFragments,
635
+ truncateMeetingSummary,
636
+ formatMeetingSummaryBullets,
637
+ scoreMeetingTakeaway,
638
+ collectMeetingTakeaways,
639
+ collectMeetingNextSteps,
640
+ buildTimedOutMeetingConclusion,
618
641
  };
@@ -5,12 +5,13 @@
5
5
  */
6
6
 
7
7
  const fs = require('fs');
8
+ const os = require('os');
8
9
  const path = require('path');
9
10
  const shared = require('./shared');
10
11
  const queries = require('./queries');
11
12
 
12
13
  const { safeJson, safeRead, getProjects, log, ts, dateStamp, truncateTextBytes, ENGINE_DEFAULTS, WI_STATUS, WORK_TYPE, PR_STATUS, DISPATCH_RESULT } = shared;
13
- const { getConfig, getDispatch, getNotes, getAgentCharter, getPrs, AGENTS_DIR } = queries;
14
+ const { getConfig, getDispatch, getNotes, getAgentCharter, getPrs, getKnowledgeBaseIndex, AGENTS_DIR } = queries;
14
15
 
15
16
  const MINIONS_DIR = shared.MINIONS_DIR;
16
17
  const PLAYBOOKS_DIR = path.join(MINIONS_DIR, 'playbooks');
@@ -394,8 +395,8 @@ function renderPlaybook(type, vars) {
394
395
  content += '````\n```skill\n';
395
396
  content += `---\nname: short-descriptive-name\ndescription: One-line description of what this skill does\nallowed-tools: Bash, Read, Edit\ntrigger: when should an agent use this\nscope: minions\nproject: any\n---\n\n# Skill Title\n\n## Steps\n1. ...\n2. ...\n\n## Notes\n...\n`;
396
397
  content += '```\n````\n\n';
397
- content += `- Set \`scope: minions\` for cross-project or Minions-wide skills; the engine writes them to ~/.claude/skills/ so they are available in normal Claude windows too\n`;
398
- content += `- Set \`scope: project\` + \`project: <name>\` only for repo-specific skills; the engine queues a PR to <project>/.claude/skills/\n`;
398
+ content += `- Set \`scope: minions\` for cross-project or Minions-wide skills; the engine writes them to the selected runtime's native personal skills directory\n`;
399
+ content += `- Set \`scope: project\` + \`project: <name>\` only for repo-specific skills; the engine queues a PR to the selected runtime's native project skills directory\n`;
399
400
  content += `- Emit at most one skill block per task unless you uncovered two clearly distinct reusable workflows\n`;
400
401
  content += `- Do NOT create a skill for one-off bug fixes, isolated command output, obvious repo facts, or anything already covered by existing docs/playbooks/skills\n`;
401
402
 
@@ -503,7 +504,7 @@ function buildSystemPrompt(agentId, config, project) {
503
504
  prompt += `3. Follow the project conventions in CLAUDE.md if present\n`;
504
505
  prompt += `4. Write learnings to the path specified in the task prompt (format: \`notes/inbox/{agent}-{work-item-id}-{date}-{time}.md\`)\n`;
505
506
  prompt += `5. Agent status is managed by the engine via dispatch.json — agents do not need to track their own status\n`;
506
- prompt += `6. If you discover a repeatable workflow, output it as a \\\`\\\`\\\`skill fenced block — minions-scoped skills are auto-extracted to ~/.claude/skills/ so they are available in normal Claude windows too\n\n`;
507
+ prompt += `6. If you discover a repeatable workflow, output it as a \\\`\\\`\\\`skill fenced block — minions-scoped skills are auto-extracted to the selected runtime's native personal skills directory\n\n`;
507
508
 
508
509
  return prompt;
509
510
  }
@@ -514,6 +515,26 @@ function buildAgentContext(agentId, config, project) {
514
515
  project = project || getProjects(config)[0] || {};
515
516
  let context = '';
516
517
 
518
+ function appendContextFile(heading, filePath, maxBytes, extra = '') {
519
+ if (!filePath) return;
520
+ const content = safeRead(filePath);
521
+ if (!content || !content.trim()) return;
522
+ const truncated = Buffer.byteLength(content, 'utf8') > maxBytes
523
+ ? truncateTextBytes(content, maxBytes, '\n\n_...truncated; read the full file if needed_')
524
+ : content;
525
+ context += `## ${heading}\n\n`;
526
+ if (extra) context += `${extra}\n\n`;
527
+ context += `${truncated}\n\n`;
528
+ }
529
+
530
+ function appendIndex(heading, body, maxBytes) {
531
+ if (!body || !String(body).trim()) return;
532
+ const truncated = Buffer.byteLength(body, 'utf8') > maxBytes
533
+ ? truncateTextBytes(body, maxBytes, '\n\n_...index truncated; use Glob/Read for the full list_')
534
+ : body;
535
+ context += `## ${heading}\n\n${truncated.replace(/^## .+\n\n/, '')}\n`;
536
+ }
537
+
517
538
 
518
539
  // Agent history — last 5 tasks only (keeps it relevant, avoids 37KB dumps)
519
540
  const history = safeRead(path.join(AGENTS_DIR, agentId, 'history.md'));
@@ -527,16 +548,19 @@ function buildAgentContext(agentId, config, project) {
527
548
 
528
549
  // Project conventions (from CLAUDE.md) — always relevant for code quality
529
550
  if (project.localPath) {
530
- const claudeMd = safeRead(path.join(project.localPath, 'CLAUDE.md'));
531
- if (claudeMd && claudeMd.trim()) {
532
- const truncated = claudeMd.length > 8192 ? claudeMd.slice(0, 8192) + '\n\n...(truncated)' : claudeMd;
533
- context += `## Project Conventions (from CLAUDE.md)\n\n${truncated}\n\n`;
534
- }
551
+ appendContextFile('Project Conventions (from CLAUDE.md)', path.join(project.localPath, 'CLAUDE.md'), 8192);
552
+ appendContextFile('Project Agent Instructions (from AGENTS.md)', path.join(project.localPath, 'AGENTS.md'), 8192,
553
+ 'These instructions are explicitly injected because some runtimes suppress automatic AGENTS.md loading. Follow them unless they conflict with the Minions task contract or playbook.');
554
+ appendContextFile('Project Copilot Instructions (from .github/copilot-instructions.md)', path.join(project.localPath, '.github', 'copilot-instructions.md'), 8192,
555
+ 'Follow these repository instructions unless they conflict with the Minions task contract or playbook.');
535
556
  }
536
557
 
537
- // KB and skills: NOT injected agents can Glob/Read when needed
538
- // This saves ~27KB per dispatch. Reference note so agents know they exist:
539
- context += `## Reference Files\n\nKnowledge base entries are in \`knowledge/{category}/*.md\`. User-level Minions skills live in \`~/.claude/skills/\`, and project-specific skills live in \`<project>/.claude/skills/\`. Use Glob/Read when relevant.\n\n`;
558
+ appendContextFile('User Claude Instructions (from ~/.claude/CLAUDE.md)', path.join(os.homedir(), '.claude', 'CLAUDE.md'), 8192,
559
+ 'These are the user-level Claude Code instructions available in regular Claude usage. Follow them unless they conflict with the Minions task contract or playbook.');
560
+
561
+ appendIndex('Knowledge Base Reference', getKnowledgeBaseIndex(), 8192);
562
+
563
+ context += `## Reference Files\n\nKnowledge base entries are in \`knowledge/{category}/*.md\`, and project-local playbooks live in \`projects/<project>/playbooks/\`. Runtime-native skills and commands are left to the selected CLI runtime; Minions does not inject their contents into the task prompt.\n\n`;
540
564
 
541
565
  // Minions awareness: what's in flight, who's doing what
542
566
  const dispatch = getDispatch();
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,
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1681",
3
+ "version": "0.1.1682",
4
4
  "description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
5
5
  "bin": {
6
6
  "minions": "bin/minions.js"