@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/engine/github.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engine/github.js — GitHub integration for Minions engine.
|
|
3
|
+
* Parallel to ado.js: PR status polling, comment polling, reconciliation for GitHub-hosted projects.
|
|
4
|
+
* Uses `gh` CLI for all GitHub API calls.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const shared = require('./shared');
|
|
8
|
+
const { exec, getProjects, projectPrPath, projectWorkItemsPath, safeJson, safeWrite, MINIONS_DIR } = shared;
|
|
9
|
+
const { getPrs } = require('./queries');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
// Lazy require to avoid circular dependency
|
|
13
|
+
let _engine = null;
|
|
14
|
+
function engine() {
|
|
15
|
+
if (!_engine) _engine = require('../engine');
|
|
16
|
+
return _engine;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
function isGitHub(project) {
|
|
22
|
+
return project?.repoHost === 'github';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Get GitHub owner/repo slug from project config (e.g. "x3-design/Bebop_Workspaces") */
|
|
26
|
+
function getRepoSlug(project) {
|
|
27
|
+
const org = project.adoOrg || '';
|
|
28
|
+
const repo = project.repoName || '';
|
|
29
|
+
if (!org || !repo) return null;
|
|
30
|
+
return `${org}/${repo}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Run a `gh api` call and parse JSON result. Returns null on failure. */
|
|
34
|
+
function ghApi(endpoint, slug) {
|
|
35
|
+
try {
|
|
36
|
+
const cmd = `gh api "repos/${slug}${endpoint}"`;
|
|
37
|
+
const result = exec(cmd, { timeout: 30000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
38
|
+
return JSON.parse(result);
|
|
39
|
+
} catch (e) {
|
|
40
|
+
engine().log('warn', `GitHub API error (${endpoint}): ${e.message}`);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── Shared PR Polling Loop ─────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
async function forEachActiveGhPr(config, callback) {
|
|
48
|
+
const projects = getProjects(config).filter(isGitHub);
|
|
49
|
+
let totalUpdated = 0;
|
|
50
|
+
|
|
51
|
+
for (const project of projects) {
|
|
52
|
+
const slug = getRepoSlug(project);
|
|
53
|
+
if (!slug) continue;
|
|
54
|
+
|
|
55
|
+
const prs = getPrs(project);
|
|
56
|
+
const activePrs = prs.filter(pr => pr.status === 'active');
|
|
57
|
+
if (activePrs.length === 0) continue;
|
|
58
|
+
|
|
59
|
+
let projectUpdated = 0;
|
|
60
|
+
|
|
61
|
+
for (const pr of activePrs) {
|
|
62
|
+
const prNum = (pr.id || '').replace('PR-', '');
|
|
63
|
+
if (!prNum) continue;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const updated = await callback(project, pr, prNum, slug);
|
|
67
|
+
if (updated) projectUpdated++;
|
|
68
|
+
} catch (err) {
|
|
69
|
+
try { engine().log('warn', `GitHub: failed to poll PR ${pr.id}: ${err.message}`); } catch {}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (projectUpdated > 0) {
|
|
74
|
+
safeWrite(projectPrPath(project), prs);
|
|
75
|
+
totalUpdated += projectUpdated;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return totalUpdated;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── PR Status Polling ──────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
async function pollPrStatus(config) {
|
|
85
|
+
const e = engine();
|
|
86
|
+
|
|
87
|
+
const totalUpdated = await forEachActiveGhPr(config, async (project, pr, prNum, slug) => {
|
|
88
|
+
const prData = ghApi(`/pulls/${prNum}`, slug);
|
|
89
|
+
if (!prData) return false;
|
|
90
|
+
|
|
91
|
+
let updated = false;
|
|
92
|
+
|
|
93
|
+
// Map GitHub PR state to minions status
|
|
94
|
+
let newStatus = pr.status;
|
|
95
|
+
if (prData.merged) newStatus = 'merged';
|
|
96
|
+
else if (prData.state === 'closed') newStatus = 'abandoned';
|
|
97
|
+
else if (prData.state === 'open') newStatus = 'active';
|
|
98
|
+
|
|
99
|
+
if (pr.status !== newStatus) {
|
|
100
|
+
e.log('info', `PR ${pr.id} status: ${pr.status} → ${newStatus}`);
|
|
101
|
+
pr.status = newStatus;
|
|
102
|
+
updated = true;
|
|
103
|
+
|
|
104
|
+
if (newStatus === 'merged' || newStatus === 'abandoned') {
|
|
105
|
+
await engine().handlePostMerge(pr, project, config, newStatus);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Review status from GitHub reviews
|
|
110
|
+
const reviews = ghApi(`/pulls/${prNum}/reviews`, slug);
|
|
111
|
+
if (reviews && Array.isArray(reviews)) {
|
|
112
|
+
// Get latest review per user
|
|
113
|
+
const latestByUser = new Map();
|
|
114
|
+
for (const r of reviews) {
|
|
115
|
+
const user = r.user?.login || '';
|
|
116
|
+
if (r.state === 'COMMENTED') continue; // Skip plain comments
|
|
117
|
+
latestByUser.set(user, r.state);
|
|
118
|
+
}
|
|
119
|
+
const states = [...latestByUser.values()];
|
|
120
|
+
|
|
121
|
+
let newReviewStatus = pr.reviewStatus || 'pending';
|
|
122
|
+
if (states.some(s => s === 'CHANGES_REQUESTED')) newReviewStatus = 'changes-requested';
|
|
123
|
+
else if (states.some(s => s === 'APPROVED')) newReviewStatus = 'approved';
|
|
124
|
+
else if (states.length > 0) newReviewStatus = 'pending';
|
|
125
|
+
|
|
126
|
+
if (pr.reviewStatus !== newReviewStatus) {
|
|
127
|
+
e.log('info', `PR ${pr.id} reviewStatus: ${pr.reviewStatus} → ${newReviewStatus}`);
|
|
128
|
+
pr.reviewStatus = newReviewStatus;
|
|
129
|
+
updated = true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check status / checks
|
|
134
|
+
if (prData.state === 'open' && prData.head?.sha) {
|
|
135
|
+
const checksData = ghApi(`/commits/${prData.head.sha}/check-runs`, slug);
|
|
136
|
+
if (checksData && checksData.check_runs) {
|
|
137
|
+
const runs = checksData.check_runs;
|
|
138
|
+
let buildStatus = 'none';
|
|
139
|
+
let buildFailReason = '';
|
|
140
|
+
|
|
141
|
+
if (runs.length > 0) {
|
|
142
|
+
const hasFailed = runs.some(r => r.conclusion === 'failure' || r.conclusion === 'timed_out');
|
|
143
|
+
const allDone = runs.every(r => r.status === 'completed');
|
|
144
|
+
const allPassed = runs.every(r => r.conclusion === 'success' || r.conclusion === 'skipped' || r.conclusion === 'neutral');
|
|
145
|
+
|
|
146
|
+
if (hasFailed) {
|
|
147
|
+
buildStatus = 'failing';
|
|
148
|
+
const failed = runs.find(r => r.conclusion === 'failure' || r.conclusion === 'timed_out');
|
|
149
|
+
buildFailReason = failed?.name || 'Check failed';
|
|
150
|
+
} else if (allDone && allPassed) {
|
|
151
|
+
buildStatus = 'passing';
|
|
152
|
+
} else {
|
|
153
|
+
buildStatus = 'running';
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (pr.buildStatus !== buildStatus) {
|
|
158
|
+
e.log('info', `PR ${pr.id} build: ${pr.buildStatus || 'none'} → ${buildStatus}${buildFailReason ? ' (' + buildFailReason + ')' : ''}`);
|
|
159
|
+
pr.buildStatus = buildStatus;
|
|
160
|
+
if (buildFailReason) pr.buildFailReason = buildFailReason;
|
|
161
|
+
else delete pr.buildFailReason;
|
|
162
|
+
updated = true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return updated;
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (totalUpdated > 0) {
|
|
171
|
+
e.log('info', `GitHub PR status poll: updated ${totalUpdated} PR(s)`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ─── Poll Human Comments on PRs ─────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
async function pollPrHumanComments(config) {
|
|
178
|
+
const e = engine();
|
|
179
|
+
|
|
180
|
+
const totalUpdated = await forEachActiveGhPr(config, async (project, pr, prNum, slug) => {
|
|
181
|
+
// Get issue comments (general PR comments)
|
|
182
|
+
const comments = ghApi(`/issues/${prNum}/comments`, slug);
|
|
183
|
+
if (!comments || !Array.isArray(comments)) return false;
|
|
184
|
+
|
|
185
|
+
// Also get review comments (inline code comments)
|
|
186
|
+
const reviewComments = ghApi(`/pulls/${prNum}/comments`, slug);
|
|
187
|
+
const allComments = [
|
|
188
|
+
...(comments || []).map(c => ({ ...c, _type: 'issue' })),
|
|
189
|
+
...((reviewComments || []).map(c => ({ ...c, _type: 'review' })))
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
// Filter out bot comments and minions's own comments
|
|
193
|
+
const humanComments = allComments.filter(c => {
|
|
194
|
+
if (c.user?.type === 'Bot') return false;
|
|
195
|
+
if (/\bMinions\s*\(/i.test(c.body || '')) return false;
|
|
196
|
+
return true;
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const cutoff = pr.humanFeedback?.lastProcessedCommentDate || pr.created || '1970-01-01';
|
|
200
|
+
|
|
201
|
+
// Collect ALL human comments for full context, track new ones for triggering
|
|
202
|
+
const allCommentEntries = [];
|
|
203
|
+
const newComments = [];
|
|
204
|
+
|
|
205
|
+
for (const c of humanComments) {
|
|
206
|
+
const date = c.created_at || c.updated_at || '';
|
|
207
|
+
const entry = {
|
|
208
|
+
commentId: c.id,
|
|
209
|
+
author: c.user?.login || 'Human',
|
|
210
|
+
content: c.body || '',
|
|
211
|
+
date
|
|
212
|
+
};
|
|
213
|
+
allCommentEntries.push(entry);
|
|
214
|
+
|
|
215
|
+
// Any new comment triggers a fix — no @minions filter needed
|
|
216
|
+
if (date > cutoff) {
|
|
217
|
+
newComments.push(entry);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (newComments.length === 0) return false;
|
|
222
|
+
|
|
223
|
+
// Sort all comments chronologically and build full context for the fix agent
|
|
224
|
+
allCommentEntries.sort((a, b) => a.date.localeCompare(b.date));
|
|
225
|
+
newComments.sort((a, b) => a.date.localeCompare(b.date));
|
|
226
|
+
const latestDate = newComments[newComments.length - 1].date;
|
|
227
|
+
|
|
228
|
+
// Provide ALL comments as context — the agent needs full thread context to fix properly
|
|
229
|
+
const feedbackContent = allCommentEntries
|
|
230
|
+
.map(c => {
|
|
231
|
+
const isNew = c.date > cutoff;
|
|
232
|
+
return `${isNew ? '**[NEW]** ' : ''}**${c.author}** (${c.date}):\n${c.content.replace(/@minions\s*/gi, '').trim()}`;
|
|
233
|
+
})
|
|
234
|
+
.join('\n\n---\n\n');
|
|
235
|
+
|
|
236
|
+
pr.humanFeedback = {
|
|
237
|
+
lastProcessedCommentDate: latestDate,
|
|
238
|
+
pendingFix: true,
|
|
239
|
+
feedbackContent
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
e.log('info', `PR ${pr.id}: ${newComments.length} new comment(s), ${allCommentEntries.length} total — full thread context provided`);
|
|
243
|
+
return true;
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
if (totalUpdated > 0) {
|
|
247
|
+
e.log('info', `GitHub PR comment poll: found human feedback on ${totalUpdated} PR(s)`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ─── PR Reconciliation ──────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
async function reconcilePrs(config) {
|
|
254
|
+
const e = engine();
|
|
255
|
+
const projects = getProjects(config).filter(isGitHub);
|
|
256
|
+
const branchPatterns = [/^work\//i, /^feat\//i, /^user\/yemishin\//i];
|
|
257
|
+
let totalAdded = 0;
|
|
258
|
+
|
|
259
|
+
for (const project of projects) {
|
|
260
|
+
const slug = getRepoSlug(project);
|
|
261
|
+
if (!slug) continue;
|
|
262
|
+
|
|
263
|
+
// Fetch open PRs
|
|
264
|
+
const prsData = ghApi('/pulls?state=open&per_page=100', slug);
|
|
265
|
+
if (!prsData || !Array.isArray(prsData)) continue;
|
|
266
|
+
|
|
267
|
+
const ghPrs = prsData.filter(pr => {
|
|
268
|
+
const branch = pr.head?.ref || '';
|
|
269
|
+
return branchPatterns.some(pat => pat.test(branch));
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
if (ghPrs.length === 0) continue;
|
|
273
|
+
|
|
274
|
+
const prPath = projectPrPath(project);
|
|
275
|
+
const existingPrs = safeJson(prPath) || [];
|
|
276
|
+
const existingIds = new Set(existingPrs.map(p => p.id));
|
|
277
|
+
let projectAdded = 0;
|
|
278
|
+
|
|
279
|
+
// Load work items to match branches
|
|
280
|
+
const wiPath = projectWorkItemsPath(project);
|
|
281
|
+
const workItems = safeJson(wiPath) || [];
|
|
282
|
+
const centralWiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
283
|
+
const centralItems = safeJson(centralWiPath) || [];
|
|
284
|
+
const allItems = [...workItems, ...centralItems];
|
|
285
|
+
|
|
286
|
+
for (const ghPr of ghPrs) {
|
|
287
|
+
const prId = `PR-${ghPr.number}`;
|
|
288
|
+
if (existingIds.has(prId)) continue;
|
|
289
|
+
|
|
290
|
+
const branch = ghPr.head?.ref || '';
|
|
291
|
+
const wiMatch = branch.match(/(P-[a-f0-9]{6,})/i) || branch.match(/(PL-W\d+)/i);
|
|
292
|
+
const linkedItemId = wiMatch ? wiMatch[1] : null;
|
|
293
|
+
const linkedItem = linkedItemId ? allItems.find(i => i.id === linkedItemId) : null;
|
|
294
|
+
const confirmedItemId = linkedItem ? linkedItemId : null;
|
|
295
|
+
|
|
296
|
+
const prUrl = project.prUrlBase ? project.prUrlBase + ghPr.number : ghPr.html_url || '';
|
|
297
|
+
|
|
298
|
+
existingPrs.push({
|
|
299
|
+
id: prId,
|
|
300
|
+
title: (ghPr.title || `PR #${ghPr.number}`).slice(0, 120),
|
|
301
|
+
agent: (ghPr.user?.login || 'unknown').toLowerCase(),
|
|
302
|
+
branch,
|
|
303
|
+
reviewStatus: 'pending',
|
|
304
|
+
status: 'active',
|
|
305
|
+
created: (ghPr.created_at || '').slice(0, 10) || e.dateStamp(),
|
|
306
|
+
url: prUrl,
|
|
307
|
+
prdItems: confirmedItemId ? [confirmedItemId] : [],
|
|
308
|
+
});
|
|
309
|
+
existingIds.add(prId);
|
|
310
|
+
projectAdded++;
|
|
311
|
+
|
|
312
|
+
e.log('info', `GitHub PR reconciliation: added ${prId} (branch: ${branch}${confirmedItemId ? ', linked to ' + confirmedItemId : ''}) to ${project.name}`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (projectAdded > 0) {
|
|
316
|
+
safeWrite(prPath, existingPrs);
|
|
317
|
+
totalAdded += projectAdded;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (totalAdded > 0) {
|
|
322
|
+
e.log('info', `GitHub PR reconciliation: added ${totalAdded} missing PR(s)`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
module.exports = {
|
|
327
|
+
pollPrStatus,
|
|
328
|
+
pollPrHumanComments,
|
|
329
|
+
reconcilePrs,
|
|
330
|
+
};
|
|
331
|
+
|