@yemi33/minions 0.1.1681 → 0.1.1683
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 +16 -0
- package/dashboard/js/render-skills.js +55 -9
- package/dashboard/js/render-work-items.js +0 -1
- package/dashboard/pages/tools.html +1 -1
- package/dashboard.js +37 -9
- package/engine/copilot-models.json +1 -1
- package/engine/lifecycle.js +29 -8
- package/engine/meeting.js +36 -13
- package/engine/playbook.js +36 -12
- package/engine/queries.js +254 -67
- package/engine/runtimes/claude.js +52 -0
- package/engine/runtimes/copilot.js +69 -0
- package/engine/spawn-agent.js +10 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.1683 (2026-05-02)
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
- rename Claude tab, merge plugins by runtime, fix detection and escaping
|
|
7
|
+
|
|
8
|
+
## 0.1.1682 (2026-05-02)
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
- index Copilot plugin skills and surface as separate dashboard tab
|
|
12
|
+
|
|
13
|
+
### Other
|
|
14
|
+
- test(meeting): add direct unit tests for internal scoring/formatting/path-resolution helpers (#1985)
|
|
15
|
+
- docs: document new optional runtime adapter methods for skills
|
|
16
|
+
- Keep runtime skills adapter-owned
|
|
17
|
+
- docs: update copilot instructions
|
|
18
|
+
|
|
3
19
|
## 0.1.1681 (2026-05-02)
|
|
4
20
|
|
|
5
21
|
### Features
|
|
@@ -4,6 +4,33 @@ let _skillsTab = 'all';
|
|
|
4
4
|
let _skillsPage = 0;
|
|
5
5
|
const SKILLS_PER_PAGE = 5;
|
|
6
6
|
|
|
7
|
+
// Per-runtime tabs that fold native and plugin skills into one bucket — agents
|
|
8
|
+
// see "skills my Claude/Copilot install can use" rather than discovery-source
|
|
9
|
+
// trivia. Project tabs are added dynamically per configured project.
|
|
10
|
+
const SOURCE_META = {
|
|
11
|
+
'claude-code': { icon: '⚡', label: 'claude', group: 'claude' },
|
|
12
|
+
'plugin': { icon: '🔌', label: 'claude plugin', group: 'claude' },
|
|
13
|
+
'copilot': { icon: '🤖', label: 'copilot', group: 'copilot' },
|
|
14
|
+
'copilot-plugin': { icon: '🔌', label: 'copilot plugin', group: 'copilot' },
|
|
15
|
+
'agent-skill': { icon: '🤝', label: 'agent', group: 'agent' },
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function _skillMetaOf(s) {
|
|
19
|
+
if (SOURCE_META[s]) return SOURCE_META[s];
|
|
20
|
+
if (s && s.startsWith('project:')) {
|
|
21
|
+
const name = s.slice('project:'.length);
|
|
22
|
+
return { icon: '📁', label: name, group: name };
|
|
23
|
+
}
|
|
24
|
+
return { icon: '🔧', label: 'unknown', group: 'unknown' };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Inline onclick handlers run in HTML-attr-then-JS context. escHtml alone is
|
|
28
|
+
// unsafe (apostrophes survive into the JS string). JSON.stringify produces a
|
|
29
|
+
// valid JS literal; HTML-escaping that literal is safe in either context.
|
|
30
|
+
function _jsArg(value) {
|
|
31
|
+
return escHtml(JSON.stringify(value == null ? '' : String(value)));
|
|
32
|
+
}
|
|
33
|
+
|
|
7
34
|
function renderSkills(skills) {
|
|
8
35
|
const el = document.getElementById('skills-list');
|
|
9
36
|
const countEl = document.getElementById('skills-count');
|
|
@@ -13,17 +40,21 @@ function renderSkills(skills) {
|
|
|
13
40
|
return;
|
|
14
41
|
}
|
|
15
42
|
|
|
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';
|
|
18
|
-
|
|
19
43
|
// Group by source
|
|
20
44
|
const groups = {};
|
|
21
45
|
for (const r of skills) {
|
|
22
|
-
const key = r.source
|
|
46
|
+
const key = _skillMetaOf(r.source).group;
|
|
23
47
|
if (!groups[key]) groups[key] = [];
|
|
24
48
|
groups[key].push(r);
|
|
25
49
|
}
|
|
26
|
-
const groupKeys = Object.keys(groups).sort((a, b) =>
|
|
50
|
+
const groupKeys = Object.keys(groups).sort((a, b) => {
|
|
51
|
+
const order = ['claude', 'copilot', 'agent'];
|
|
52
|
+
const ai = order.indexOf(a), bi = order.indexOf(b);
|
|
53
|
+
if (ai >= 0 && bi >= 0) return ai - bi;
|
|
54
|
+
if (ai >= 0) return -1;
|
|
55
|
+
if (bi >= 0) return 1;
|
|
56
|
+
return a.localeCompare(b);
|
|
57
|
+
});
|
|
27
58
|
|
|
28
59
|
// Tab bar
|
|
29
60
|
const tabs = [{ key: 'all', label: 'All (' + skills.length + ')' }];
|
|
@@ -34,10 +65,18 @@ function renderSkills(skills) {
|
|
|
34
65
|
const active = _skillsTab === t.key;
|
|
35
66
|
html += '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;' +
|
|
36
67
|
(active ? 'background:var(--green);color:#fff;border-color:var(--green)' : '') +
|
|
37
|
-
'" onclick="
|
|
68
|
+
'" onclick="_skillsSelectTab(' + _jsArg(t.key) + ')">' + escHtml(t.label) + '</button>';
|
|
38
69
|
}
|
|
39
70
|
html += '</div>';
|
|
40
71
|
|
|
72
|
+
// Note clarifying skill visibility — agents read these on demand, runtime
|
|
73
|
+
// assets (~/.claude/skills, ~/.copilot/skills) are not auto-injected for
|
|
74
|
+
// the OTHER runtime. A Copilot agent only sees Copilot-native + plugin skills.
|
|
75
|
+
html += '<div style="font-size:9px;color:var(--muted);margin-bottom:8px;line-height:1.4">' +
|
|
76
|
+
'Skills are reference docs agents read on demand — they are not injected wholesale into prompts. ' +
|
|
77
|
+
'Each tab reflects what the matching runtime would see; cross-runtime skills are NOT visible to a different runtime.' +
|
|
78
|
+
'</div>';
|
|
79
|
+
|
|
41
80
|
// Filter by tab
|
|
42
81
|
const filtered = _skillsTab === 'all' ? skills : (groups[_skillsTab] || []);
|
|
43
82
|
|
|
@@ -48,10 +87,11 @@ function renderSkills(skills) {
|
|
|
48
87
|
|
|
49
88
|
html += '<div style="display:flex;flex-direction:column;gap:6px">';
|
|
50
89
|
for (const r of page) {
|
|
90
|
+
const meta = _skillMetaOf(r.source);
|
|
51
91
|
const autoTag = r.autoGenerated ? '<span style="font-size:8px;background:rgba(63,185,80,0.15);color:var(--green);padding:1px 4px;border-radius:3px;margin-left:4px">auto</span>' : '';
|
|
52
|
-
html += '<div class="inbox-item" onclick="openSkill(
|
|
53
|
-
'<div class="inbox-name"><span style="color:var(--green);font-weight:600">' +
|
|
54
|
-
'<span style="font-size:9px;color:var(--muted);margin-left:auto">' + escHtml(
|
|
92
|
+
html += '<div class="inbox-item" onclick="openSkill(' + _jsArg(r.file) + ',' + _jsArg(r.source || 'claude-code') + ',' + _jsArg(r.dir || '') + ')" style="border-left-color:var(--green)">' +
|
|
93
|
+
'<div class="inbox-name"><span style="color:var(--green);font-weight:600">' + meta.icon + ' ' + escHtml(r.name) + '</span>' + autoTag +
|
|
94
|
+
'<span style="font-size:9px;color:var(--muted);margin-left:auto">' + escHtml(meta.label) + '</span>' +
|
|
55
95
|
'</div>' +
|
|
56
96
|
(r.description ? '<div class="inbox-preview" style="color:var(--text)">' + escHtml(r.description) + '</div>' : '') +
|
|
57
97
|
'</div>';
|
|
@@ -75,6 +115,12 @@ function renderSkills(skills) {
|
|
|
75
115
|
window._lastSkills = skills;
|
|
76
116
|
}
|
|
77
117
|
|
|
118
|
+
function _skillsSelectTab(key) {
|
|
119
|
+
_skillsTab = key;
|
|
120
|
+
_skillsPage = 0;
|
|
121
|
+
renderSkills(window._lastSkills);
|
|
122
|
+
}
|
|
123
|
+
|
|
78
124
|
function openSkill(file, source, dir) {
|
|
79
125
|
document.getElementById('modal-title').textContent = file;
|
|
80
126
|
document.getElementById('modal-body').innerHTML = '<p style="color:var(--muted)">Loading...</p>';
|
|
@@ -498,7 +498,6 @@ function openWorkItemDetail(id) {
|
|
|
498
498
|
}
|
|
499
499
|
});
|
|
500
500
|
}
|
|
501
|
-
if (arts.skills && arts.skills.length > 0) arts.skills.forEach(function(s) { artPills += '<span onclick="openSkill(\'' + escapeHtml(s) + '\',\'minions\',\'\')" style="' + pillStyle + '">⚙ ' + escapeHtml(s) + '</span> '; });
|
|
502
501
|
if (artPills) html += field('Artifacts', '<div style="display:flex;flex-wrap:wrap;gap:4px">' + artPills + '</div>');
|
|
503
502
|
|
|
504
503
|
if (item._totalCostUsd != null) html += field('Cumulative Cost', '$' + Number(item._totalCostUsd).toFixed(4));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<section>
|
|
2
|
-
<h2>Minions Skills <span class="count" id="skills-count">0</span> <span style="font-size:10px;color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">
|
|
2
|
+
<h2>Minions Skills <span class="count" id="skills-count">0</span> <span style="font-size:10px;color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">discovered from runtime native dirs, plugin installs, and configured project repos</span></h2>
|
|
3
3
|
<div id="skills-list"><p class="empty">No skills yet. Agents create these when they discover repeatable workflows.</p></div>
|
|
4
4
|
</section>
|
|
5
5
|
<section>
|
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
|
-
|
|
176
|
-
if (
|
|
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
|
|
4502
|
-
|
|
4503
|
-
if (!
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
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', '*');
|
package/engine/lifecycle.js
CHANGED
|
@@ -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
|
|
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 \`${
|
|
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,22 @@ function extractSkillsFromOutput(output, agentId, dispatchItem, config) {
|
|
|
1682
1699
|
}
|
|
1683
1700
|
}
|
|
1684
1701
|
} else {
|
|
1685
|
-
|
|
1686
|
-
const
|
|
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
|
-
//
|
|
1706
|
+
// Native skill format: only name + description in frontmatter. The
|
|
1707
|
+
// `Auto-extracted` marker is an HTML comment so the dashboard's
|
|
1708
|
+
// autoGenerated detection picks it up without polluting the body
|
|
1709
|
+
// an agent reads.
|
|
1691
1710
|
const description = m('description') || m('trigger') || `Auto-extracted skill from ${agentName}`;
|
|
1692
1711
|
const body = fmMatch[2] || '';
|
|
1693
|
-
const
|
|
1712
|
+
const marker = `<!-- Auto-extracted by ${agentName} on ${dateStamp()} -->`;
|
|
1713
|
+
const ccContent = `---\nname: ${name}\ndescription: ${description}\n---\n\n${marker}\n\n${body.trim()}\n`;
|
|
1694
1714
|
if (!fs.existsSync(skillDir)) fs.mkdirSync(skillDir, { recursive: true });
|
|
1695
1715
|
shared.safeWrite(skillPath, ccContent);
|
|
1696
|
-
|
|
1716
|
+
try { require('./queries').invalidateSkillsCache(); } catch {}
|
|
1717
|
+
log('info', `Extracted skill "${name}" from ${agentName} → ${skillPath}`);
|
|
1697
1718
|
} else {
|
|
1698
1719
|
log('info', `Skill "${name}" already exists, skipping`);
|
|
1699
1720
|
}
|
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
|
|
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(
|
|
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
|
|
130
|
-
.split(/\n+|
|
|
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
|
-
|
|
137
|
-
|
|
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
|
|
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 (
|
|
157
|
-
if (
|
|
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
|
|
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
|
};
|
package/engine/playbook.js
CHANGED
|
@@ -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
|
|
398
|
-
content += `- Set \`scope: project\` + \`project: <name>\` only for repo-specific skills; the engine queues a PR to
|
|
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
|
|
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
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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.
|
|
731
|
-
|
|
732
|
-
const
|
|
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
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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
|
-
}
|
|
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
|
|
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/spawn-agent.js
CHANGED
|
@@ -174,15 +174,18 @@ function main() {
|
|
|
174
174
|
opts.sysPromptFile = sysTmpPath;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
//
|
|
178
|
-
// worktree, so
|
|
179
|
-
//
|
|
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
|
|
181
|
+
const runtimeAssetDirs = typeof runtime.getUserAssetDirs === 'function'
|
|
182
|
+
? runtime.getUserAssetDirs({ homeDir: os.homedir() })
|
|
183
|
+
: [];
|
|
183
184
|
const addDirs = [minionsDir];
|
|
184
|
-
|
|
185
|
-
|
|
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.
|
|
3
|
+
"version": "0.1.1683",
|
|
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"
|