@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.
Files changed (54) hide show
  1. package/CHANGELOG.md +819 -0
  2. package/LICENSE +21 -0
  3. package/README.md +598 -0
  4. package/agents/dallas/charter.md +56 -0
  5. package/agents/lambert/charter.md +67 -0
  6. package/agents/ralph/charter.md +45 -0
  7. package/agents/rebecca/charter.md +57 -0
  8. package/agents/ripley/charter.md +47 -0
  9. package/bin/minions.js +467 -0
  10. package/config.template.json +28 -0
  11. package/dashboard.html +4822 -0
  12. package/dashboard.js +2623 -0
  13. package/docs/auto-discovery.md +416 -0
  14. package/docs/blog-first-successful-dispatch.md +128 -0
  15. package/docs/command-center.md +156 -0
  16. package/docs/demo/01-dashboard-overview.gif +0 -0
  17. package/docs/demo/02-command-center.gif +0 -0
  18. package/docs/demo/03-work-items.gif +0 -0
  19. package/docs/demo/04-plan-docchat.gif +0 -0
  20. package/docs/demo/05-prd-progress.gif +0 -0
  21. package/docs/demo/06-inbox-metrics.gif +0 -0
  22. package/docs/deprecated.json +83 -0
  23. package/docs/distribution.md +96 -0
  24. package/docs/engine-restart.md +92 -0
  25. package/docs/human-vs-automated.md +108 -0
  26. package/docs/index.html +221 -0
  27. package/docs/plan-lifecycle.md +140 -0
  28. package/docs/self-improvement.md +344 -0
  29. package/engine/ado-mcp-wrapper.js +42 -0
  30. package/engine/ado.js +383 -0
  31. package/engine/check-status.js +23 -0
  32. package/engine/cli.js +754 -0
  33. package/engine/consolidation.js +417 -0
  34. package/engine/github.js +331 -0
  35. package/engine/lifecycle.js +1113 -0
  36. package/engine/llm.js +116 -0
  37. package/engine/queries.js +677 -0
  38. package/engine/shared.js +397 -0
  39. package/engine/spawn-agent.js +151 -0
  40. package/engine.js +3227 -0
  41. package/minions.js +556 -0
  42. package/package.json +48 -0
  43. package/playbooks/ask.md +49 -0
  44. package/playbooks/build-and-test.md +155 -0
  45. package/playbooks/explore.md +64 -0
  46. package/playbooks/fix.md +57 -0
  47. package/playbooks/implement-shared.md +68 -0
  48. package/playbooks/implement.md +95 -0
  49. package/playbooks/plan-to-prd.md +104 -0
  50. package/playbooks/plan.md +99 -0
  51. package/playbooks/review.md +68 -0
  52. package/playbooks/test.md +75 -0
  53. package/playbooks/verify.md +190 -0
  54. package/playbooks/work-item.md +74 -0
@@ -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
+