@yemi33/minions 0.1.1680 → 0.1.1682
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dashboard/js/render-skills.js +18 -3
- package/dashboard.js +98 -53
- package/docs/plan-lifecycle.md +23 -7
- package/engine/copilot-models.json +1 -1
- package/engine/lifecycle.js +105 -165
- 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/shared.js +28 -0
- package/engine/spawn-agent.js +10 -7
- package/engine.js +50 -31
- package/package.json +1 -1
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/shared.js
CHANGED
|
@@ -212,6 +212,33 @@ function safeUnlink(p) {
|
|
|
212
212
|
try { fs.unlinkSync(p); } catch { /* cleanup */ }
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
function neutralizeJsonBackupSidecar(filePath, inertData = { status: 'archived' }) {
|
|
216
|
+
const backupPath = filePath + '.backup';
|
|
217
|
+
try {
|
|
218
|
+
fs.unlinkSync(backupPath);
|
|
219
|
+
return { ok: true, action: 'removed', backupPath };
|
|
220
|
+
} catch (unlinkErr) {
|
|
221
|
+
if (unlinkErr.code === 'ENOENT') return { ok: true, action: 'absent', backupPath };
|
|
222
|
+
try {
|
|
223
|
+
safeWrite(backupPath, inertData);
|
|
224
|
+
return {
|
|
225
|
+
ok: true,
|
|
226
|
+
action: 'neutralized',
|
|
227
|
+
backupPath,
|
|
228
|
+
unlinkError: unlinkErr.message,
|
|
229
|
+
};
|
|
230
|
+
} catch (writeErr) {
|
|
231
|
+
return {
|
|
232
|
+
ok: false,
|
|
233
|
+
action: 'failed',
|
|
234
|
+
backupPath,
|
|
235
|
+
unlinkError: unlinkErr.message,
|
|
236
|
+
writeError: writeErr.message,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
215
242
|
// ── Dispatch Prompt Sidecar (#1167) ─────────────────────────────────────────
|
|
216
243
|
// Large prompts (PR diffs, build error logs, coalesced human feedback) inlined
|
|
217
244
|
// into dispatch.json caused hundreds-of-MB bloat per entry and eventual V8 OOM
|
|
@@ -2387,6 +2414,7 @@ module.exports = {
|
|
|
2387
2414
|
safeJson, safeJsonObj, safeJsonArr,
|
|
2388
2415
|
safeWrite,
|
|
2389
2416
|
safeUnlink,
|
|
2417
|
+
neutralizeJsonBackupSidecar,
|
|
2390
2418
|
PROMPT_CONTEXTS_DIR,
|
|
2391
2419
|
dispatchPromptSidecarPath,
|
|
2392
2420
|
dispatchCompletionReportPath,
|
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;
|