@yemi33/minions 0.1.1682 → 0.1.1684

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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1684 (2026-05-02)
4
+
5
+ ### Other
6
+ - test(cooldown): add edge-case unit tests for _truncateContextEntry, loadCooldowns boundaries, isAlreadyDispatched (#1986)
7
+
8
+ ## 0.1.1683 (2026-05-02)
9
+
10
+ ### Fixes
11
+ - rename Claude tab, merge plugins by runtime, fix detection and escaping
12
+
3
13
  ## 0.1.1682 (2026-05-02)
4
14
 
5
15
  ### 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,32 +40,21 @@ function renderSkills(skills) {
13
40
  return;
14
41
  }
15
42
 
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;
33
-
34
43
  // Group by source
35
44
  const groups = {};
36
45
  for (const r of skills) {
37
- const key = metaOf(r.source).group;
46
+ const key = _skillMetaOf(r.source).group;
38
47
  if (!groups[key]) groups[key] = [];
39
48
  groups[key].push(r);
40
49
  }
41
- const groupKeys = Object.keys(groups).sort((a, b) => a === 'global' ? -1 : b === 'global' ? 1 : a.localeCompare(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
+ });
42
58
 
43
59
  // Tab bar
44
60
  const tabs = [{ key: 'all', label: 'All (' + skills.length + ')' }];
@@ -49,10 +65,18 @@ function renderSkills(skills) {
49
65
  const active = _skillsTab === t.key;
50
66
  html += '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;' +
51
67
  (active ? 'background:var(--green);color:#fff;border-color:var(--green)' : '') +
52
- '" onclick="_skillsTab=\'' + escHtml(t.key) + '\';_skillsPage=0;renderSkills(window._lastSkills)">' + escHtml(t.label) + '</button>';
68
+ '" onclick="_skillsSelectTab(' + _jsArg(t.key) + ')">' + escHtml(t.label) + '</button>';
53
69
  }
54
70
  html += '</div>';
55
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
+
56
80
  // Filter by tab
57
81
  const filtered = _skillsTab === 'all' ? skills : (groups[_skillsTab] || []);
58
82
 
@@ -63,10 +87,11 @@ function renderSkills(skills) {
63
87
 
64
88
  html += '<div style="display:flex;flex-direction:column;gap:6px">';
65
89
  for (const r of page) {
90
+ const meta = _skillMetaOf(r.source);
66
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>' : '';
67
- html += '<div class="inbox-item" onclick="openSkill(\'' + escHtml(r.file) + '\',\'' + escHtml(r.source || 'claude-code') + '\',\'' + escHtml(r.dir || '') + '\')" style="border-left-color:var(--green)">' +
68
- '<div class="inbox-name"><span style="color:var(--green);font-weight:600">' + sourceIcon(r.source) + ' ' + escHtml(r.name) + '</span>' + autoTag +
69
- '<span style="font-size:9px;color:var(--muted);margin-left:auto">' + escHtml(sourceLabel(r.source)) + '</span>' +
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>' +
70
95
  '</div>' +
71
96
  (r.description ? '<div class="inbox-preview" style="color:var(--text)">' + escHtml(r.description) + '</div>' : '') +
72
97
  '</div>';
@@ -90,6 +115,12 @@ function renderSkills(skills) {
90
115
  window._lastSkills = skills;
91
116
  }
92
117
 
118
+ function _skillsSelectTab(key) {
119
+ _skillsTab = key;
120
+ _skillsPage = 0;
121
+ renderSkills(window._lastSkills);
122
+ }
123
+
93
124
  function openSkill(file, source, dir) {
94
125
  document.getElementById('modal-title').textContent = file;
95
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">auto-discovered from project repos</span></h2>
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>
@@ -201,4 +201,5 @@ module.exports = {
201
201
  clearCooldown,
202
202
  isAlreadyDispatched,
203
203
  isBranchActive,
204
+ _truncateContextEntry, // exported for testing
204
205
  };
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-02T15:15:12.425Z"
4
+ "cachedAt": "2026-05-02T16:47:10.817Z"
5
5
  }
@@ -1703,10 +1703,14 @@ function extractSkillsFromOutput(output, agentId, dispatchItem, config, runtimeN
1703
1703
  const skillDir = path.join(personalSkillRoot, name.replace(/[^a-z0-9-]/g, '-'));
1704
1704
  const skillPath = path.join(skillDir, 'SKILL.md');
1705
1705
  if (!fs.existsSync(skillPath)) {
1706
- // Native skill format: only name + description in frontmatter.
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.
1707
1710
  const description = m('description') || m('trigger') || `Auto-extracted skill from ${agentName}`;
1708
1711
  const body = fmMatch[2] || '';
1709
- const ccContent = `---\nname: ${name}\ndescription: ${description}\n---\n\n${body.trim()}\n`;
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`;
1710
1714
  if (!fs.existsSync(skillDir)) fs.mkdirSync(skillDir, { recursive: true });
1711
1715
  shared.safeWrite(skillPath, ccContent);
1712
1716
  try { require('./queries').invalidateSkillsCache(); } catch {}
@@ -395,7 +395,7 @@ function renderPlaybook(type, vars) {
395
395
  content += '````\n```skill\n';
396
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`;
397
397
  content += '```\n````\n\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`;
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 so they are available in normal runtime windows too\n`;
399
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`;
400
400
  content += `- Emit at most one skill block per task unless you uncovered two clearly distinct reusable workflows\n`;
401
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`;
@@ -546,7 +546,8 @@ function buildAgentContext(agentId, config, project) {
546
546
  context += `## Your Recent History (last 5 tasks)\n\n${trimmed}\n\n`;
547
547
  }
548
548
 
549
- // Project conventions (from CLAUDE.md)always relevant for code quality
549
+ // Project conventions and instruction files explicit, inert context for
550
+ // every runtime. Runtime-native skills/commands stay in their native indexes.
550
551
  if (project.localPath) {
551
552
  appendContextFile('Project Conventions (from CLAUDE.md)', path.join(project.localPath, 'CLAUDE.md'), 8192);
552
553
  appendContextFile('Project Agent Instructions (from AGENTS.md)', path.join(project.localPath, 'AGENTS.md'), 8192,
@@ -16,6 +16,9 @@
16
16
  * - spawnScript: absolute path of the spawn wrapper (or null if direct-only)
17
17
  * - buildArgs(opts) → string[] — CLI args excluding the binary
18
18
  * - buildPrompt(promptText, sysPromptText) → string — final prompt delivered
19
+ * - getUserAssetDirs(opts) → string[] — runtime-native global asset roots
20
+ * - getSkillRoots(opts) → {scope,dir,projectName?}[] — skill discovery roots
21
+ * - getSkillWriteTargets(opts) → {personal,project} — extraction targets
19
22
  * - resolveModel(input) → string|undefined — shorthand expansion / passthrough
20
23
  * - parseOutput(raw) → { text, usage, sessionId, model }
21
24
  * - parseStreamChunk(line) → parsed event object or null
@@ -247,6 +250,31 @@ function buildSpawnFlags(opts = {}) {
247
250
  return flags;
248
251
  }
249
252
 
253
+ function getUserAssetDirs({ homeDir = os.homedir() } = {}) {
254
+ return [path.join(homeDir, '.claude')];
255
+ }
256
+
257
+ function getSkillRoots({ homeDir = os.homedir(), project = null } = {}) {
258
+ const roots = [
259
+ { scope: 'claude-code', dir: path.join(homeDir, '.claude', 'skills') },
260
+ ];
261
+ if (project?.localPath) {
262
+ roots.push({
263
+ scope: 'project',
264
+ projectName: project.name,
265
+ dir: path.join(project.localPath, '.claude', 'skills'),
266
+ });
267
+ }
268
+ return roots;
269
+ }
270
+
271
+ function getSkillWriteTargets({ homeDir = os.homedir(), project = null } = {}) {
272
+ return {
273
+ personal: path.join(homeDir, '.claude', 'skills'),
274
+ project: project?.localPath ? path.join(project.localPath, '.claude', 'skills') : null,
275
+ };
276
+ }
277
+
250
278
  function getResumeSessionId({ agentId, branchName, agentsDir, maxAgeMs = 2 * 60 * 60 * 1000, logger = console } = {}) {
251
279
  if (!agentId || agentId.startsWith('temp-') || !agentsDir) return null;
252
280
  try {
@@ -322,29 +350,6 @@ function buildPrompt(promptText, /* sysPromptText */ _sys) {
322
350
  return String(promptText == null ? '' : promptText);
323
351
  }
324
352
 
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
-
348
353
  // ── Output Parsing ───────────────────────────────────────────────────────────
349
354
 
350
355
  /**
@@ -270,6 +270,42 @@ function buildSpawnFlags(opts = {}) {
270
270
  return flags;
271
271
  }
272
272
 
273
+ function getUserAssetDirs({ homeDir = os.homedir() } = {}) {
274
+ return [
275
+ path.join(homeDir, '.copilot'),
276
+ path.join(homeDir, '.agents'),
277
+ ];
278
+ }
279
+
280
+ function getSkillRoots({ homeDir = os.homedir(), project = null } = {}) {
281
+ const roots = [
282
+ { scope: 'copilot', dir: path.join(homeDir, '.copilot', 'skills') },
283
+ { scope: 'agent-skill', dir: path.join(homeDir, '.agents', 'skills') },
284
+ ];
285
+ if (project?.localPath) {
286
+ roots.push(
287
+ {
288
+ scope: 'project',
289
+ projectName: project.name,
290
+ dir: path.join(project.localPath, '.github', 'skills'),
291
+ },
292
+ {
293
+ scope: 'project',
294
+ projectName: project.name,
295
+ dir: path.join(project.localPath, '.agents', 'skills'),
296
+ },
297
+ );
298
+ }
299
+ return roots;
300
+ }
301
+
302
+ function getSkillWriteTargets({ homeDir = os.homedir(), project = null } = {}) {
303
+ return {
304
+ personal: path.join(homeDir, '.copilot', 'skills'),
305
+ project: project?.localPath ? path.join(project.localPath, '.github', 'skills') : null,
306
+ };
307
+ }
308
+
273
309
  function getResumeSessionId({ agentId, branchName, agentsDir, maxAgeMs = 2 * 60 * 60 * 1000, logger = console } = {}) {
274
310
  if (!agentId || agentId.startsWith('temp-') || !agentsDir) return null;
275
311
  try {
@@ -349,41 +385,6 @@ function buildPrompt(promptText, sysPromptText, opts = {}) {
349
385
  return `<system>\n${String(sysPromptText)}\n</system>\n\n${user}`;
350
386
  }
351
387
 
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
-
387
388
  // ── Output Parsing ──────────────────────────────────────────────────────────
388
389
  //
389
390
  // Whitelist of event types observed during the spike (docs/copilot-cli-schema.md
@@ -174,17 +174,17 @@ function main() {
174
174
  opts.sysPromptFile = sysTmpPath;
175
175
  }
176
176
 
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.
177
+ // Skill discovery dirs — agents run with CWD set to an external repo
178
+ // worktree, so runtime-native global assets would otherwise be invisible.
179
+ // The adapter owns both where those assets live and how to surface them.
180
180
  const minionsDir = path.resolve(__dirname, '..');
181
+ const addDirs = [minionsDir];
181
182
  const runtimeAssetDirs = typeof runtime.getUserAssetDirs === 'function'
182
183
  ? runtime.getUserAssetDirs({ homeDir: os.homedir() })
183
184
  : [];
184
- const addDirs = [minionsDir];
185
- for (const userAssetDir of runtimeAssetDirs) {
186
- if (fs.existsSync(userAssetDir) && path.resolve(userAssetDir) !== path.resolve(minionsDir)) {
187
- addDirs.push(userAssetDir);
185
+ for (const dir of runtimeAssetDirs) {
186
+ if (dir && fs.existsSync(dir) && path.resolve(dir) !== path.resolve(minionsDir)) {
187
+ addDirs.push(dir);
188
188
  }
189
189
  }
190
190
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1682",
3
+ "version": "0.1.1684",
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"