@yemi33/minions 0.1.1
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 +819 -0
- package/LICENSE +21 -0
- package/README.md +598 -0
- package/agents/dallas/charter.md +56 -0
- package/agents/lambert/charter.md +67 -0
- package/agents/ralph/charter.md +45 -0
- package/agents/rebecca/charter.md +57 -0
- package/agents/ripley/charter.md +47 -0
- package/bin/minions.js +467 -0
- package/config.template.json +28 -0
- package/dashboard.html +4822 -0
- package/dashboard.js +2623 -0
- package/docs/auto-discovery.md +416 -0
- package/docs/blog-first-successful-dispatch.md +128 -0
- package/docs/command-center.md +156 -0
- package/docs/demo/01-dashboard-overview.gif +0 -0
- package/docs/demo/02-command-center.gif +0 -0
- package/docs/demo/03-work-items.gif +0 -0
- package/docs/demo/04-plan-docchat.gif +0 -0
- package/docs/demo/05-prd-progress.gif +0 -0
- package/docs/demo/06-inbox-metrics.gif +0 -0
- package/docs/deprecated.json +83 -0
- package/docs/distribution.md +96 -0
- package/docs/engine-restart.md +92 -0
- package/docs/human-vs-automated.md +108 -0
- package/docs/index.html +221 -0
- package/docs/plan-lifecycle.md +140 -0
- package/docs/self-improvement.md +344 -0
- package/engine/ado-mcp-wrapper.js +42 -0
- package/engine/ado.js +383 -0
- package/engine/check-status.js +23 -0
- package/engine/cli.js +754 -0
- package/engine/consolidation.js +417 -0
- package/engine/github.js +331 -0
- package/engine/lifecycle.js +1113 -0
- package/engine/llm.js +116 -0
- package/engine/queries.js +677 -0
- package/engine/shared.js +397 -0
- package/engine/spawn-agent.js +151 -0
- package/engine.js +3227 -0
- package/minions.js +556 -0
- package/package.json +48 -0
- package/playbooks/ask.md +49 -0
- package/playbooks/build-and-test.md +155 -0
- package/playbooks/explore.md +64 -0
- package/playbooks/fix.md +57 -0
- package/playbooks/implement-shared.md +68 -0
- package/playbooks/implement.md +95 -0
- package/playbooks/plan-to-prd.md +104 -0
- package/playbooks/plan.md +99 -0
- package/playbooks/review.md +68 -0
- package/playbooks/test.md +75 -0
- package/playbooks/verify.md +190 -0
- package/playbooks/work-item.md +74 -0
package/minions.js
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Minions Init — Link a project to the central minions
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node minions.js <project-dir> Add a project interactively
|
|
7
|
+
* node minions.js <project-dir> --remove Remove a project
|
|
8
|
+
* node minions.js --list List linked projects
|
|
9
|
+
*
|
|
10
|
+
* This adds the project to ~/.minions/config.json's projects array.
|
|
11
|
+
* The minions engine and dashboard run centrally from ~/.minions/.
|
|
12
|
+
* Each project just needs its own work-items.json and pull-requests.json.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const readline = require('readline');
|
|
18
|
+
const { execSync } = require('child_process');
|
|
19
|
+
const { ENGINE_DEFAULTS, DEFAULT_AGENTS, DEFAULT_CLAUDE } = require('./engine/shared');
|
|
20
|
+
|
|
21
|
+
const MINIONS_HOME = __dirname;
|
|
22
|
+
const CONFIG_PATH = path.join(MINIONS_HOME, 'config.json');
|
|
23
|
+
|
|
24
|
+
function loadConfig() {
|
|
25
|
+
try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); } catch {
|
|
26
|
+
return { projects: [], engine: {}, claude: {}, agents: {} };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function cleanupPlaceholderProjects(config) {
|
|
31
|
+
const projects = Array.isArray(config?.projects) ? config.projects : [];
|
|
32
|
+
const filtered = projects.filter(p => {
|
|
33
|
+
const name = String(p?.name || '').trim();
|
|
34
|
+
return name && name !== 'YOUR_PROJECT_NAME';
|
|
35
|
+
});
|
|
36
|
+
const removed = projects.length - filtered.length;
|
|
37
|
+
if (removed > 0) config.projects = filtered;
|
|
38
|
+
return removed;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function saveConfig(config) {
|
|
42
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
46
|
+
function ask(q, def) {
|
|
47
|
+
return new Promise(resolve => {
|
|
48
|
+
rl.question(` ${q}${def ? ` [${def}]` : ''}: `, ans => resolve(ans.trim() || def || ''));
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function autoDiscover(targetDir) {
|
|
53
|
+
const result = { _found: [] };
|
|
54
|
+
|
|
55
|
+
// 1. Detect main branch from git
|
|
56
|
+
try {
|
|
57
|
+
const head = execSync('git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || git symbolic-ref HEAD', { cwd: targetDir, encoding: 'utf8', timeout: 5000 }).trim();
|
|
58
|
+
const branch = head.replace('refs/remotes/origin/', '').replace('refs/heads/', '');
|
|
59
|
+
if (branch) { result.mainBranch = branch; result._found.push('main branch'); }
|
|
60
|
+
} catch {}
|
|
61
|
+
|
|
62
|
+
// 2. Detect repo host, org, project, repo name from git remote URL
|
|
63
|
+
try {
|
|
64
|
+
const remoteUrl = execSync('git remote get-url origin', { cwd: targetDir, encoding: 'utf8', timeout: 5000 }).trim();
|
|
65
|
+
if (remoteUrl.includes('github.com')) {
|
|
66
|
+
result.repoHost = 'github';
|
|
67
|
+
// https://github.com/org/repo.git or git@github.com:org/repo.git
|
|
68
|
+
const m = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
|
|
69
|
+
if (m) { result.org = m[1]; result.repoName = m[2]; }
|
|
70
|
+
result._found.push('GitHub remote');
|
|
71
|
+
} else if (remoteUrl.includes('visualstudio.com') || remoteUrl.includes('dev.azure.com')) {
|
|
72
|
+
result.repoHost = 'ado';
|
|
73
|
+
// https://org.visualstudio.com/project/_git/repo or https://dev.azure.com/org/project/_git/repo
|
|
74
|
+
const m1 = remoteUrl.match(/https:\/\/([^.]+)\.visualstudio\.com[^/]*\/([^/]+)\/_git\/([^/\s]+)/);
|
|
75
|
+
const m2 = remoteUrl.match(/https:\/\/dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/([^/\s]+)/);
|
|
76
|
+
const m = m1 || m2;
|
|
77
|
+
if (m) { result.org = m[1]; result.project = m[2]; result.repoName = m[3]; }
|
|
78
|
+
result._found.push('Azure DevOps remote');
|
|
79
|
+
}
|
|
80
|
+
} catch {}
|
|
81
|
+
|
|
82
|
+
// 3. Read description from CLAUDE.md first line or README.md first paragraph
|
|
83
|
+
try {
|
|
84
|
+
const claudeMdPath = path.join(targetDir, 'CLAUDE.md');
|
|
85
|
+
if (fs.existsSync(claudeMdPath)) {
|
|
86
|
+
const content = fs.readFileSync(claudeMdPath, 'utf8');
|
|
87
|
+
// Look for a description-like first line or paragraph (skip headings)
|
|
88
|
+
const lines = content.split('\n').filter(l => l.trim() && !l.startsWith('#'));
|
|
89
|
+
if (lines[0] && lines[0].length < 200) {
|
|
90
|
+
result.description = lines[0].trim();
|
|
91
|
+
result._found.push('description from CLAUDE.md');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch {}
|
|
95
|
+
if (!result.description) {
|
|
96
|
+
try {
|
|
97
|
+
const readmePath = path.join(targetDir, 'README.md');
|
|
98
|
+
if (fs.existsSync(readmePath)) {
|
|
99
|
+
const content = fs.readFileSync(readmePath, 'utf8').slice(0, 2000);
|
|
100
|
+
const lines = content.split('\n').filter(l => l.trim() && !l.startsWith('#') && !l.startsWith('!'));
|
|
101
|
+
if (lines[0] && lines[0].length < 200) {
|
|
102
|
+
result.description = lines[0].trim();
|
|
103
|
+
result._found.push('description from README.md');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} catch {}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 4. Detect project name
|
|
110
|
+
try {
|
|
111
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
112
|
+
if (fs.existsSync(pkgPath)) {
|
|
113
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
114
|
+
if (pkg.name) { result.name = pkg.name.replace(/^@[^/]+\//, ''); result._found.push('name from package.json'); }
|
|
115
|
+
}
|
|
116
|
+
} catch {}
|
|
117
|
+
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─── Shared Helpers (used by both addProject and scanAndAdd) ─────────────────
|
|
122
|
+
|
|
123
|
+
function buildPrUrlBase({ repoHost, org, project, repoName }) {
|
|
124
|
+
if (repoHost === 'github') {
|
|
125
|
+
return org && repoName ? `https://github.com/${org}/${repoName}/pull/` : '';
|
|
126
|
+
}
|
|
127
|
+
return org && project && repoName
|
|
128
|
+
? `https://${org}.visualstudio.com/DefaultCollection/${project}/_git/${repoName}/pullrequest/`
|
|
129
|
+
: '';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function buildProjectEntry({ name, description, localPath, repoHost, repositoryId, org, project, repoName, mainBranch }) {
|
|
133
|
+
return {
|
|
134
|
+
name,
|
|
135
|
+
description: description || '',
|
|
136
|
+
localPath: (localPath || '').replace(/\\/g, '/'),
|
|
137
|
+
repoHost: repoHost || 'ado',
|
|
138
|
+
repositoryId: repositoryId || '',
|
|
139
|
+
adoOrg: org || '',
|
|
140
|
+
adoProject: project || '',
|
|
141
|
+
repoName: repoName || name,
|
|
142
|
+
mainBranch: mainBranch || 'main',
|
|
143
|
+
prUrlBase: buildPrUrlBase({ repoHost, org, project, repoName }),
|
|
144
|
+
workSources: {
|
|
145
|
+
pullRequests: { enabled: true, path: '.minions/pull-requests.json', cooldownMinutes: 30 },
|
|
146
|
+
workItems: { enabled: true, path: '.minions/work-items.json', cooldownMinutes: 0 },
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function ensureProjectStateFiles(projectPath) {
|
|
152
|
+
const minionsDir = path.join(projectPath, '.minions');
|
|
153
|
+
if (!fs.existsSync(minionsDir)) fs.mkdirSync(minionsDir, { recursive: true });
|
|
154
|
+
for (const f of ['pull-requests.json', 'work-items.json']) {
|
|
155
|
+
const fp = path.join(minionsDir, f);
|
|
156
|
+
if (!fs.existsSync(fp)) fs.writeFileSync(fp, '[]');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ─── Commands ────────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
async function addProject(targetDir) {
|
|
163
|
+
const target = path.resolve(targetDir);
|
|
164
|
+
if (!fs.existsSync(target)) {
|
|
165
|
+
console.log(` Error: Directory not found: ${target}`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const config = loadConfig();
|
|
170
|
+
if (!config.projects) config.projects = [];
|
|
171
|
+
|
|
172
|
+
// Check if already linked
|
|
173
|
+
const existing = config.projects.find(p => path.resolve(p.localPath) === target);
|
|
174
|
+
if (existing) {
|
|
175
|
+
console.log(` "${existing.name}" is already linked at ${target}`);
|
|
176
|
+
const update = await ask('Update its configuration? (y/N)', 'N');
|
|
177
|
+
if (update.toLowerCase() !== 'y') { rl.close(); return; }
|
|
178
|
+
config.projects = config.projects.filter(p => path.resolve(p.localPath) !== target);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
console.log('\n Link Project to Minions');
|
|
182
|
+
console.log(' ─────────────────────────────');
|
|
183
|
+
console.log(` Minions home: ${MINIONS_HOME}`);
|
|
184
|
+
console.log(` Project: ${target}\n`);
|
|
185
|
+
|
|
186
|
+
const detected = autoDiscover(target);
|
|
187
|
+
if (detected._found.length > 0) {
|
|
188
|
+
console.log(` Auto-detected: ${detected._found.join(', ')}\n`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const name = await ask('Project name', detected.name || path.basename(target));
|
|
192
|
+
const description = await ask('Description (what this repo contains/does)', detected.description || '');
|
|
193
|
+
const repoHost = await ask('Repo host (ado/github)', detected.repoHost || 'ado');
|
|
194
|
+
const org = await ask('Organization', detected.org || '');
|
|
195
|
+
const project = await ask('Project', detected.project || '');
|
|
196
|
+
const repoName = await ask('Repo name', detected.repoName || name);
|
|
197
|
+
const repositoryId = await ask('Repository ID (GUID, optional)', '');
|
|
198
|
+
const mainBranch = await ask('Main branch', detected.mainBranch || 'main');
|
|
199
|
+
|
|
200
|
+
rl.close();
|
|
201
|
+
|
|
202
|
+
config.projects.push(buildProjectEntry({ name, description, localPath: target, repoHost, repositoryId, org, project, repoName, mainBranch }));
|
|
203
|
+
saveConfig(config);
|
|
204
|
+
ensureProjectStateFiles(target);
|
|
205
|
+
|
|
206
|
+
console.log(`\n Linked "${name}" (${target})`);
|
|
207
|
+
console.log(` Total projects: ${config.projects.length}`);
|
|
208
|
+
console.log(`\n Start the minions from anywhere:`);
|
|
209
|
+
console.log(` node ${MINIONS_HOME}/engine.js # Engine`);
|
|
210
|
+
console.log(` node ${MINIONS_HOME}/dashboard.js # Dashboard`);
|
|
211
|
+
console.log(` node ${MINIONS_HOME}/engine.js status # Status\n`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function removeProject(targetDir) {
|
|
215
|
+
const target = path.resolve(targetDir);
|
|
216
|
+
const config = loadConfig();
|
|
217
|
+
const before = (config.projects || []).length;
|
|
218
|
+
config.projects = (config.projects || []).filter(p => path.resolve(p.localPath) !== target);
|
|
219
|
+
const after = config.projects.length;
|
|
220
|
+
|
|
221
|
+
if (before === after) {
|
|
222
|
+
console.log(` No project linked at ${target}`);
|
|
223
|
+
} else {
|
|
224
|
+
saveConfig(config);
|
|
225
|
+
console.log(` Removed project at ${target}`);
|
|
226
|
+
console.log(` Remaining projects: ${config.projects.length}`);
|
|
227
|
+
}
|
|
228
|
+
rl.close();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function listProjects() {
|
|
232
|
+
const config = loadConfig();
|
|
233
|
+
const projects = config.projects || [];
|
|
234
|
+
console.log(`\n Minions Projects (${projects.length})\n`);
|
|
235
|
+
if (projects.length === 0) {
|
|
236
|
+
console.log(' No projects linked. Run: node minions.js <project-dir>\n');
|
|
237
|
+
rl.close();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
for (const p of projects) {
|
|
241
|
+
const exists = fs.existsSync(p.localPath);
|
|
242
|
+
console.log(` ${p.name}`);
|
|
243
|
+
if (p.description) console.log(` Desc: ${p.description}`);
|
|
244
|
+
console.log(` Path: ${p.localPath} ${exists ? '' : '(NOT FOUND)'}`);
|
|
245
|
+
console.log(` Repo: ${p.adoOrg}/${p.adoProject}/${p.repoName} (${p.repoHost || 'ado'})`);
|
|
246
|
+
console.log(` ID: ${p.repositoryId || 'none'}`);
|
|
247
|
+
console.log('');
|
|
248
|
+
}
|
|
249
|
+
rl.close();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ─── Scan & Multi-Select ─────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
function findGitRepos(rootDir, maxDepth = 3) {
|
|
255
|
+
const repos = [];
|
|
256
|
+
const visited = new Set();
|
|
257
|
+
|
|
258
|
+
function walk(dir, depth) {
|
|
259
|
+
if (depth > maxDepth || visited.has(dir)) return;
|
|
260
|
+
visited.add(dir);
|
|
261
|
+
try {
|
|
262
|
+
// Skip common non-project dirs
|
|
263
|
+
const base = path.basename(dir);
|
|
264
|
+
if (['node_modules', '.git', '.hg', 'AppData', '$Recycle.Bin', 'Windows', 'Program Files',
|
|
265
|
+
'Program Files (x86)', '.cache', '.npm', '.yarn', '.nuget', 'worktrees'].includes(base)) return;
|
|
266
|
+
|
|
267
|
+
const gitDir = path.join(dir, '.git');
|
|
268
|
+
if (fs.existsSync(gitDir)) {
|
|
269
|
+
repos.push(dir);
|
|
270
|
+
return; // Don't recurse into git repos (they may have nested submodules)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
274
|
+
for (const entry of entries) {
|
|
275
|
+
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
|
276
|
+
walk(path.join(dir, entry.name), depth + 1);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
} catch {} // permission errors, etc.
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
walk(rootDir, 0);
|
|
283
|
+
return repos;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function scanAndAdd({ root, depth } = {}) {
|
|
287
|
+
const homeDir = process.env.USERPROFILE || process.env.HOME || '';
|
|
288
|
+
const scanRoot = root ? path.resolve(root) : homeDir;
|
|
289
|
+
const maxDepth = depth !== undefined ? (parseInt(depth, 10) || 3) : 3;
|
|
290
|
+
|
|
291
|
+
console.log(`\n Scanning for git repos in: ${scanRoot}`);
|
|
292
|
+
console.log(` Max depth: ${maxDepth}\n`);
|
|
293
|
+
|
|
294
|
+
const repos = findGitRepos(scanRoot, maxDepth);
|
|
295
|
+
if (repos.length === 0) {
|
|
296
|
+
console.log(' No git repositories found.\n');
|
|
297
|
+
rl.close();
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const config = loadConfig();
|
|
302
|
+
const linkedPaths = new Set((config.projects || []).map(p => path.resolve(p.localPath)));
|
|
303
|
+
|
|
304
|
+
// Enrich repos with auto-discovered metadata
|
|
305
|
+
const enriched = repos.map(repoPath => {
|
|
306
|
+
const detected = autoDiscover(repoPath);
|
|
307
|
+
const alreadyLinked = linkedPaths.has(path.resolve(repoPath));
|
|
308
|
+
return {
|
|
309
|
+
path: repoPath,
|
|
310
|
+
name: detected.name || detected.repoName || path.basename(repoPath),
|
|
311
|
+
host: detected.repoHost || '?',
|
|
312
|
+
org: detected.org || '',
|
|
313
|
+
project: detected.project || '',
|
|
314
|
+
repoName: detected.repoName || path.basename(repoPath),
|
|
315
|
+
mainBranch: detected.mainBranch || 'main',
|
|
316
|
+
description: detected.description || '',
|
|
317
|
+
linked: alreadyLinked,
|
|
318
|
+
};
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
console.log(` Found ${enriched.length} git repo(s):\n`);
|
|
322
|
+
enriched.forEach((r, i) => {
|
|
323
|
+
const tag = r.linked ? ' (already linked)' : '';
|
|
324
|
+
const hostTag = r.host === 'ado' ? 'ADO' : r.host === 'github' ? 'GitHub' : 'git';
|
|
325
|
+
console.log(` ${String(i + 1).padStart(3)}. ${r.name} [${hostTag}]${tag}`);
|
|
326
|
+
console.log(` ${r.path}`);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
console.log('\n Enter numbers to add (comma-separated, ranges ok, e.g. "1,3,5-7")');
|
|
330
|
+
console.log(' Or "all" to add all unlinked repos, "q" to quit.\n');
|
|
331
|
+
|
|
332
|
+
const answer = await ask('Select repos', '');
|
|
333
|
+
if (!answer || answer.toLowerCase() === 'q') {
|
|
334
|
+
console.log(' Cancelled.\n');
|
|
335
|
+
rl.close();
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Parse selection
|
|
340
|
+
let indices;
|
|
341
|
+
if (answer.toLowerCase() === 'all') {
|
|
342
|
+
indices = enriched.map((_, i) => i).filter(i => !enriched[i].linked);
|
|
343
|
+
} else {
|
|
344
|
+
indices = [];
|
|
345
|
+
for (const part of answer.split(',')) {
|
|
346
|
+
const trimmed = part.trim();
|
|
347
|
+
const rangeMatch = trimmed.match(/^(\d+)\s*-\s*(\d+)$/);
|
|
348
|
+
if (rangeMatch) {
|
|
349
|
+
const start = parseInt(rangeMatch[1]) - 1;
|
|
350
|
+
const end = parseInt(rangeMatch[2]) - 1;
|
|
351
|
+
for (let i = start; i <= end; i++) indices.push(i);
|
|
352
|
+
} else {
|
|
353
|
+
const n = parseInt(trimmed) - 1;
|
|
354
|
+
if (!isNaN(n)) indices.push(n);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Filter valid, unlinked selections
|
|
360
|
+
const toAdd = [...new Set(indices)]
|
|
361
|
+
.filter(i => i >= 0 && i < enriched.length && !enriched[i].linked)
|
|
362
|
+
.map(i => enriched[i]);
|
|
363
|
+
|
|
364
|
+
if (toAdd.length === 0) {
|
|
365
|
+
console.log(' Nothing to add.\n');
|
|
366
|
+
rl.close();
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
console.log(`\n Adding ${toAdd.length} project(s)...\n`);
|
|
371
|
+
|
|
372
|
+
for (const repo of toAdd) {
|
|
373
|
+
config.projects.push(buildProjectEntry({
|
|
374
|
+
name: repo.name, description: repo.description, localPath: repo.path,
|
|
375
|
+
repoHost: repo.host, org: repo.org, project: repo.project,
|
|
376
|
+
repoName: repo.repoName, mainBranch: repo.mainBranch,
|
|
377
|
+
}));
|
|
378
|
+
ensureProjectStateFiles(repo.path);
|
|
379
|
+
console.log(` + ${repo.name} (${repo.path})`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
saveConfig(config);
|
|
383
|
+
console.log(`\n Done. ${config.projects.length} total project(s) linked.`);
|
|
384
|
+
console.log(` Run "node minions.js list" to verify.\n`);
|
|
385
|
+
rl.close();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ─── CLI ─────────────────────────────────────────────────────────────────────
|
|
389
|
+
|
|
390
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
391
|
+
|
|
392
|
+
async function initMinions({ skipScan = false, scanRoot, scanDepth } = {}) {
|
|
393
|
+
const config = loadConfig();
|
|
394
|
+
if (!config.projects) config.projects = [];
|
|
395
|
+
const removedPlaceholders = cleanupPlaceholderProjects(config);
|
|
396
|
+
if (!config.engine) config.engine = { ...ENGINE_DEFAULTS };
|
|
397
|
+
if (!config.claude) config.claude = { ...DEFAULT_CLAUDE };
|
|
398
|
+
if (!config.agents || Object.keys(config.agents).length === 0) {
|
|
399
|
+
config.agents = { ...DEFAULT_AGENTS };
|
|
400
|
+
}
|
|
401
|
+
saveConfig(config);
|
|
402
|
+
console.log(`\n Minions initialized at ${MINIONS_HOME}`);
|
|
403
|
+
console.log(` Config, agents, and engine defaults created.\n`);
|
|
404
|
+
if (removedPlaceholders > 0) {
|
|
405
|
+
console.log(` Removed ${removedPlaceholders} placeholder project entr${removedPlaceholders === 1 ? 'y' : 'ies'} from config.\n`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (skipScan) {
|
|
409
|
+
console.log(' Skipping repo scan (--skip-scan). Run "node minions.js scan" later to link projects.\n');
|
|
410
|
+
} else {
|
|
411
|
+
// Auto-chain into scan
|
|
412
|
+
console.log(' Now let\'s find your repos...\n');
|
|
413
|
+
await scanAndAdd({ root: scanRoot, depth: scanDepth });
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
rl.close();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function nukeMinions() {
|
|
420
|
+
console.log('\n Minions Factory Reset');
|
|
421
|
+
console.log(' ===================\n');
|
|
422
|
+
|
|
423
|
+
// 1. Kill engine process
|
|
424
|
+
const controlPath = path.join(MINIONS_HOME, 'engine', 'control.json');
|
|
425
|
+
try {
|
|
426
|
+
const control = JSON.parse(fs.readFileSync(controlPath, 'utf8'));
|
|
427
|
+
if (control.pid) {
|
|
428
|
+
try {
|
|
429
|
+
process.kill(control.pid);
|
|
430
|
+
console.log(` Killed engine (PID: ${control.pid})`);
|
|
431
|
+
} catch { console.log(` Engine process ${control.pid} already dead`); }
|
|
432
|
+
}
|
|
433
|
+
} catch {}
|
|
434
|
+
|
|
435
|
+
// 2. Kill dashboard (port 7331)
|
|
436
|
+
try {
|
|
437
|
+
if (process.platform === 'win32') {
|
|
438
|
+
const { execSync } = require('child_process');
|
|
439
|
+
const out = execSync('netstat -ano | findstr :7331 | findstr LISTENING', { encoding: 'utf8', timeout: 5000, windowsHide: true });
|
|
440
|
+
const pids = [...new Set(out.split('\n').map(l => l.trim().split(/\s+/).pop()).filter(p => p && p !== '0'))];
|
|
441
|
+
for (const pid of pids) {
|
|
442
|
+
try { execSync(`taskkill /F /PID ${pid}`, { windowsHide: true, timeout: 5000 }); console.log(` Killed dashboard (PID: ${pid})`); } catch {}
|
|
443
|
+
}
|
|
444
|
+
} else {
|
|
445
|
+
const { execSync } = require('child_process');
|
|
446
|
+
try { execSync('lsof -ti:7331 | xargs kill -9 2>/dev/null', { timeout: 5000 }); console.log(' Killed dashboard'); } catch {}
|
|
447
|
+
}
|
|
448
|
+
} catch {}
|
|
449
|
+
|
|
450
|
+
// 3. Kill all agent processes (PID files in engine/tmp/)
|
|
451
|
+
const pidDir = path.join(MINIONS_HOME, 'engine', 'tmp');
|
|
452
|
+
try {
|
|
453
|
+
const pidFiles = fs.readdirSync(pidDir).filter(f => f.endsWith('.pid'));
|
|
454
|
+
for (const f of pidFiles) {
|
|
455
|
+
try {
|
|
456
|
+
const pid = parseInt(fs.readFileSync(path.join(pidDir, f), 'utf8').trim());
|
|
457
|
+
if (pid) { try { process.kill(pid); console.log(` Killed agent process (PID: ${pid})`); } catch {} }
|
|
458
|
+
} catch {}
|
|
459
|
+
}
|
|
460
|
+
} catch {}
|
|
461
|
+
|
|
462
|
+
// 4. Kill any remaining minions-related node processes
|
|
463
|
+
try {
|
|
464
|
+
const { execSync } = require('child_process');
|
|
465
|
+
if (process.platform === 'win32') {
|
|
466
|
+
// Find node processes with minions in their command line
|
|
467
|
+
const out = execSync('wmic process where "name=\'node.exe\'" get processid,commandline /format:csv', { encoding: 'utf8', timeout: 10000, windowsHide: true });
|
|
468
|
+
for (const line of out.split('\n')) {
|
|
469
|
+
if (line.includes('minions') && (line.includes('engine.js') || line.includes('dashboard.js') || line.includes('spawn-agent.js'))) {
|
|
470
|
+
const pid = line.split(',').pop()?.trim();
|
|
471
|
+
if (pid && pid !== String(process.pid)) {
|
|
472
|
+
try { execSync(`taskkill /F /PID ${pid}`, { windowsHide: true, timeout: 5000 }); console.log(` Killed minions process (PID: ${pid})`); } catch {}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
} catch {}
|
|
478
|
+
|
|
479
|
+
// 5. Delete runtime state (NOT the source code)
|
|
480
|
+
console.log('\n Cleaning runtime state...');
|
|
481
|
+
const runtimeDirs = ['projects', 'plans', 'prd', 'knowledge', 'skills', 'notes', 'identity'];
|
|
482
|
+
const runtimeFiles = ['config.json', 'work-items.json', 'notes.md', 'routing.md'];
|
|
483
|
+
const engineRuntimeFiles = ['control.json', 'dispatch.json', 'log.json', 'metrics.json', 'cooldowns.json', 'pr-links.json', 'kb-checkpoint.json', 'cc-session.json', 'doc-sessions.json'];
|
|
484
|
+
|
|
485
|
+
for (const dir of runtimeDirs) {
|
|
486
|
+
const p = path.join(MINIONS_HOME, dir);
|
|
487
|
+
if (fs.existsSync(p)) { try { fs.rmSync(p, { recursive: true, force: true }); console.log(` Deleted ${dir}/`); } catch {} }
|
|
488
|
+
}
|
|
489
|
+
for (const f of runtimeFiles) {
|
|
490
|
+
const p = path.join(MINIONS_HOME, f);
|
|
491
|
+
if (fs.existsSync(p)) { try { fs.unlinkSync(p); console.log(` Deleted ${f}`); } catch {} }
|
|
492
|
+
}
|
|
493
|
+
const engineDir = path.join(MINIONS_HOME, 'engine');
|
|
494
|
+
for (const f of engineRuntimeFiles) {
|
|
495
|
+
const p = path.join(engineDir, f);
|
|
496
|
+
if (fs.existsSync(p)) { try { fs.unlinkSync(p); console.log(` Deleted engine/${f}`); } catch {} }
|
|
497
|
+
}
|
|
498
|
+
// Clean engine/tmp/
|
|
499
|
+
const tmpDir = path.join(engineDir, 'tmp');
|
|
500
|
+
if (fs.existsSync(tmpDir)) { try { fs.rmSync(tmpDir, { recursive: true, force: true }); console.log(' Deleted engine/tmp/'); } catch {} }
|
|
501
|
+
// Clean agent history and output logs (preserve charters)
|
|
502
|
+
const agentsDir = path.join(MINIONS_HOME, 'agents');
|
|
503
|
+
if (fs.existsSync(agentsDir)) {
|
|
504
|
+
for (const agent of fs.readdirSync(agentsDir)) {
|
|
505
|
+
const agentDir = path.join(agentsDir, agent);
|
|
506
|
+
try { if (!fs.statSync(agentDir).isDirectory()) continue; } catch { continue; }
|
|
507
|
+
for (const f of fs.readdirSync(agentDir)) {
|
|
508
|
+
if (f === 'charter.md') continue; // preserve charters
|
|
509
|
+
try { fs.unlinkSync(path.join(agentDir, f)); } catch {}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
console.log(' Cleaned agent state (charters preserved)');
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
console.log(' Factory reset complete. Run "minions init" to start fresh.\n');
|
|
516
|
+
rl.close();
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const commands = {
|
|
520
|
+
init: () => {
|
|
521
|
+
const skipScanFlag = rest.includes('--skip-scan');
|
|
522
|
+
const initArgs = rest.filter(arg => arg !== '--skip-scan');
|
|
523
|
+
const [scanRoot, scanDepth] = initArgs;
|
|
524
|
+
initMinions({ skipScan: skipScanFlag, scanRoot, scanDepth })
|
|
525
|
+
.catch(e => { console.error(e); process.exit(1); });
|
|
526
|
+
},
|
|
527
|
+
add: () => {
|
|
528
|
+
const dir = rest[0];
|
|
529
|
+
if (!dir) { console.log('Usage: node minions add <project-dir>'); process.exit(1); }
|
|
530
|
+
addProject(dir).catch(e => { console.error(e); process.exit(1); });
|
|
531
|
+
},
|
|
532
|
+
remove: () => {
|
|
533
|
+
const dir = rest[0];
|
|
534
|
+
if (!dir) { console.log('Usage: node minions remove <project-dir>'); process.exit(1); }
|
|
535
|
+
removeProject(dir);
|
|
536
|
+
},
|
|
537
|
+
list: () => listProjects(),
|
|
538
|
+
scan: () => scanAndAdd({ root: rest[0], depth: rest[1] })
|
|
539
|
+
.catch(e => { console.error(e); process.exit(1); }),
|
|
540
|
+
nuke: () => nukeMinions(),
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
if (cmd && commands[cmd]) {
|
|
544
|
+
commands[cmd]();
|
|
545
|
+
} else {
|
|
546
|
+
console.log('\n Minions — Central AI dev team manager\n');
|
|
547
|
+
console.log(' Usage: node minions <command>\n');
|
|
548
|
+
console.log(' Commands:');
|
|
549
|
+
console.log(' init Initialize minions and scan for repos');
|
|
550
|
+
console.log(' scan [dir] [depth] Scan for git repos and multi-select to add');
|
|
551
|
+
console.log(' add <project-dir> Link a single project');
|
|
552
|
+
console.log(' remove <project-dir> Unlink a project');
|
|
553
|
+
console.log(' list List linked projects');
|
|
554
|
+
console.log(' nuke Factory reset — kill all processes, delete ~/.minions/\n');
|
|
555
|
+
}
|
|
556
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yemi33/minions",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
|
+
"bin": {
|
|
6
|
+
"minions": "bin/minions.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"test": "node test/unit.test.js",
|
|
10
|
+
"test:unit": "node test/unit.test.js",
|
|
11
|
+
"test:integration": "node test/minions-tests.js",
|
|
12
|
+
"test:e2e": "npx playwright test",
|
|
13
|
+
"test:e2e:headed": "npx playwright test --headed",
|
|
14
|
+
"test:e2e:ui": "npx playwright test --ui",
|
|
15
|
+
"test:e2e:report": "npx playwright show-report test/playwright/report",
|
|
16
|
+
"test:e2e:video": "npx playwright test --video=on --headed",
|
|
17
|
+
"test:all": "node test/unit.test.js && node test/minions-tests.js",
|
|
18
|
+
"test:e2e:accept": "node test/playwright/accept-baseline.js",
|
|
19
|
+
"test:e2e:accept-force": "node test/playwright/accept-baseline.js --force",
|
|
20
|
+
"test:setup": "npx playwright install chromium"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"ai",
|
|
24
|
+
"agents",
|
|
25
|
+
"claude",
|
|
26
|
+
"dev-team",
|
|
27
|
+
"automation",
|
|
28
|
+
"multi-agent",
|
|
29
|
+
"cli"
|
|
30
|
+
],
|
|
31
|
+
"author": "yemi33",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/yemi33/minions.git"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/yemi33/minions#readme",
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@playwright/test": "^1.58.2"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/playbooks/ask.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Ask Playbook
|
|
2
|
+
|
|
3
|
+
> Agent: {{agent_name}} | Question from user | ID: {{task_id}}
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
Team root: {{team_root}}
|
|
8
|
+
{{scope_section}}
|
|
9
|
+
|
|
10
|
+
## Mission
|
|
11
|
+
|
|
12
|
+
The user has asked a question. Answer it thoroughly and clearly, writing your response to the inbox so they can read it.
|
|
13
|
+
|
|
14
|
+
## Question
|
|
15
|
+
|
|
16
|
+
{{question}}
|
|
17
|
+
|
|
18
|
+
## Steps
|
|
19
|
+
|
|
20
|
+
### 1. Understand the Question
|
|
21
|
+
- Read the question carefully
|
|
22
|
+
- If it references specific files, code, or concepts — go read them first
|
|
23
|
+
- If it references a project, use the project context above to orient yourself
|
|
24
|
+
|
|
25
|
+
### 2. Research
|
|
26
|
+
- Read relevant source files, docs, configs, or history as needed
|
|
27
|
+
- Use the codebase — don't guess when you can look
|
|
28
|
+
- If the question is about "this" (ambiguous reference), check recent git history and agent activity for context
|
|
29
|
+
|
|
30
|
+
### 3. Write Your Answer
|
|
31
|
+
Write your answer to `{{team_root}}/notes/inbox/{{agent_id}}-answer-{{task_id}}-{{date}}.md` with:
|
|
32
|
+
|
|
33
|
+
- **Question**: (restate briefly)
|
|
34
|
+
- **Answer**: (clear, direct answer)
|
|
35
|
+
- **References**: (files, links, or code snippets that support the answer)
|
|
36
|
+
|
|
37
|
+
Keep it concise but complete. Write for a senior engineer who wants the real answer, not fluff.
|
|
38
|
+
|
|
39
|
+
### 4. Status
|
|
40
|
+
**Note:** Do NOT write to `agents/*/status.json` — the engine manages your status automatically.
|
|
41
|
+
|
|
42
|
+
## Rules
|
|
43
|
+
- Do NOT modify any code unless the question explicitly asks you to.
|
|
44
|
+
- Do NOT create PRs or branches — this is a read-only task.
|
|
45
|
+
- Do NOT checkout branches in the main working tree.
|
|
46
|
+
- Read `notes.md` for all team rules before starting.
|
|
47
|
+
|
|
48
|
+
## Team Decisions
|
|
49
|
+
{{notes_content}}
|