@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
package/engine/ado.js ADDED
@@ -0,0 +1,383 @@
1
+ /**
2
+ * engine/ado.js — Azure DevOps integration for Minions engine.
3
+ * Extracted from engine.js: ADO token management, PR status polling, human comment polling.
4
+ */
5
+
6
+ const shared = require('./shared');
7
+ const { exec, getAdoOrgBase, addPrLink } = shared;
8
+ const { getPrs } = require('./queries');
9
+
10
+ // Lazy require to avoid circular dependency
11
+ let _engine = null;
12
+ function engine() {
13
+ if (!_engine) _engine = require('../engine');
14
+ return _engine;
15
+ }
16
+
17
+ // ─── ADO Token Cache ─────────────────────────────────────────────────────────
18
+
19
+ let _adoTokenCache = { token: null, expiresAt: 0 };
20
+ let _adoTokenFailedUntil = 0; // backoff: skip azureauth calls until this timestamp
21
+
22
+ function getAdoToken() {
23
+ if (_adoTokenCache.token && Date.now() < _adoTokenCache.expiresAt) {
24
+ return _adoTokenCache.token;
25
+ }
26
+ // If recent fetch failed, don't retry until backoff expires (avoids repeated browser popups)
27
+ if (Date.now() < _adoTokenFailedUntil) return null;
28
+ try {
29
+ const token = exec('azureauth ado token --mode iwa --mode broker --output token --timeout 1', {
30
+ timeout: 15000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }).trim();
31
+ if (token && token.startsWith('eyJ')) {
32
+ _adoTokenCache = { token, expiresAt: Date.now() + 30 * 60 * 1000 };
33
+ _adoTokenFailedUntil = 0;
34
+ return token;
35
+ }
36
+ } catch (e) {
37
+ engine().log('warn', `Failed to get ADO token: ${e.message}`);
38
+ }
39
+ // Back off for 10 minutes to avoid spamming browser auth popups
40
+ _adoTokenFailedUntil = Date.now() + 10 * 60 * 1000;
41
+ return null;
42
+ }
43
+
44
+ async function adoFetch(url, token, _retryCount = 0) {
45
+ const MAX_RETRIES = 1;
46
+ const res = await fetch(url, {
47
+ headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }
48
+ });
49
+ if (!res.ok) throw new Error(`ADO API ${res.status}: ${res.statusText}`);
50
+ const text = await res.text();
51
+ if (!text || text.trimStart().startsWith('<')) {
52
+ // Invalidate cached token — it's likely expired
53
+ _adoTokenCache = { token: null, expiresAt: 0 };
54
+ if (_retryCount < MAX_RETRIES) {
55
+ const freshToken = getAdoToken();
56
+ if (freshToken) {
57
+ engine().log('info', 'ADO token expired mid-session — refreshed and retrying');
58
+ return adoFetch(url, freshToken, _retryCount + 1);
59
+ }
60
+ }
61
+ throw new Error(`ADO returned HTML instead of JSON (likely auth redirect) for ${url.split('?')[0]}`);
62
+ }
63
+ return JSON.parse(text);
64
+ }
65
+
66
+ // ─── Shared PR Polling Loop ──────────────────────────────────────────────────
67
+
68
+ /**
69
+ * Iterate active PRs across all projects. Calls `callback(project, pr, prNum, orgBase)`
70
+ * for each active PR. If callback returns truthy, the PR file is saved after the project loop.
71
+ */
72
+ async function forEachActivePr(config, token, callback) {
73
+ const projects = shared.getProjects(config);
74
+ let totalUpdated = 0;
75
+
76
+ for (const project of projects) {
77
+ if (!project.adoOrg || !project.adoProject || !project.repositoryId) continue;
78
+
79
+ const prs = getPrs(project);
80
+ const activePrs = prs.filter(pr => pr.status === 'active');
81
+ if (activePrs.length === 0) continue;
82
+
83
+ let projectUpdated = 0;
84
+
85
+ for (const pr of activePrs) {
86
+ const prNum = (pr.id || '').replace('PR-', '');
87
+ if (!prNum) continue;
88
+
89
+ try {
90
+ const orgBase = getAdoOrgBase(project);
91
+ const updated = await callback(project, pr, prNum, orgBase);
92
+ if (updated) projectUpdated++;
93
+ } catch (err) {
94
+ try { engine().log('warn', `Failed to poll status for ${pr.id}: ${err.message}`); } catch {}
95
+ }
96
+ }
97
+
98
+ if (projectUpdated > 0) {
99
+ shared.safeWrite(shared.projectPrPath(project), prs);
100
+ totalUpdated += projectUpdated;
101
+ }
102
+ }
103
+
104
+ return totalUpdated;
105
+ }
106
+
107
+ // ─── PR Status Polling ───────────────────────────────────────────────────────
108
+
109
+ async function pollPrStatus(config) {
110
+ const e = engine();
111
+ const token = getAdoToken();
112
+ if (!token) {
113
+ e.log('warn', 'Skipping PR status poll — no ADO token available');
114
+ return;
115
+ }
116
+
117
+ const totalUpdated = await forEachActivePr(config, token, async (project, pr, prNum, orgBase) => {
118
+ const repoBase = `${orgBase}/${project.adoProject}/_apis/git/repositories/${project.repositoryId}/pullrequests/${prNum}`;
119
+ let updated = false;
120
+
121
+ const prData = await adoFetch(`${repoBase}?api-version=7.1`, token);
122
+
123
+ let newStatus = pr.status;
124
+ if (prData.status === 'completed') newStatus = 'merged';
125
+ else if (prData.status === 'abandoned') newStatus = 'abandoned';
126
+ else if (prData.status === 'active') newStatus = 'active';
127
+
128
+ if (pr.status !== newStatus) {
129
+ e.log('info', `PR ${pr.id} status: ${pr.status} → ${newStatus}`);
130
+ pr.status = newStatus;
131
+ updated = true;
132
+
133
+ if (newStatus === 'merged' || newStatus === 'abandoned') {
134
+ await engine().handlePostMerge(pr, project, config, newStatus);
135
+ }
136
+ }
137
+
138
+ const reviewers = prData.reviewers || [];
139
+ const votes = reviewers.map(r => r.vote).filter(v => v !== undefined);
140
+ let newReviewStatus = pr.reviewStatus || 'pending';
141
+ if (votes.length > 0) {
142
+ if (votes.some(v => v === -10)) newReviewStatus = 'changes-requested';
143
+ else if (votes.some(v => v >= 5)) newReviewStatus = 'approved';
144
+ else if (votes.some(v => v === -5)) newReviewStatus = 'waiting';
145
+ else newReviewStatus = 'pending';
146
+ }
147
+
148
+ if (pr.reviewStatus !== newReviewStatus) {
149
+ e.log('info', `PR ${pr.id} reviewStatus: ${pr.reviewStatus} → ${newReviewStatus}`);
150
+ pr.reviewStatus = newReviewStatus;
151
+ updated = true;
152
+ }
153
+
154
+ if (newStatus !== 'active') return updated;
155
+
156
+ const statusData = await adoFetch(`${repoBase}/statuses?api-version=7.1`, token);
157
+
158
+ const latest = new Map();
159
+ for (const s of statusData.value || []) {
160
+ const key = (s.context?.genre || '') + '/' + (s.context?.name || '');
161
+ if (!latest.has(key)) latest.set(key, s);
162
+ }
163
+
164
+ const buildStatuses = [...latest.values()].filter(s => {
165
+ const ctx = ((s.context?.genre || '') + '/' + (s.context?.name || '')).toLowerCase();
166
+ return ctx.includes('codecoverage') || ctx.includes('build') ||
167
+ ctx.includes('deploy') || ctx.includes('ci/');
168
+ });
169
+
170
+ let buildStatus = 'none';
171
+ let buildFailReason = '';
172
+
173
+ if (buildStatuses.length > 0) {
174
+ const states = buildStatuses.map(s => s.state).filter(Boolean);
175
+ const hasFailed = states.some(s => s === 'failed' || s === 'error');
176
+ const allDone = states.every(s => s === 'succeeded' || s === 'notApplicable');
177
+ const hasQueued = buildStatuses.some(s => !s.state);
178
+
179
+ if (hasFailed) {
180
+ buildStatus = 'failing';
181
+ const failed = buildStatuses.find(s => s.state === 'failed' || s.state === 'error');
182
+ buildFailReason = failed?.description || failed?.context?.name || 'Build failed';
183
+ } else if (allDone && !hasQueued) {
184
+ buildStatus = 'passing';
185
+ } else {
186
+ buildStatus = 'running';
187
+ }
188
+ }
189
+
190
+ if (pr.buildStatus !== buildStatus) {
191
+ e.log('info', `PR ${pr.id} build: ${pr.buildStatus || 'none'} → ${buildStatus}${buildFailReason ? ' (' + buildFailReason + ')' : ''}`);
192
+ pr.buildStatus = buildStatus;
193
+ if (buildFailReason) pr.buildFailReason = buildFailReason;
194
+ else delete pr.buildFailReason;
195
+ updated = true;
196
+ }
197
+
198
+ return updated;
199
+ });
200
+
201
+ if (totalUpdated > 0) {
202
+ e.log('info', `PR status poll: updated ${totalUpdated} PR(s)`);
203
+ }
204
+ }
205
+
206
+ // ─── Poll Human Comments on PRs ──────────────────────────────────────────────
207
+
208
+ async function pollPrHumanComments(config) {
209
+ const e = engine();
210
+ const token = getAdoToken();
211
+ if (!token) return;
212
+
213
+ const totalUpdated = await forEachActivePr(config, token, async (project, pr, prNum, orgBase) => {
214
+ const threadsUrl = `${orgBase}/${project.adoProject}/_apis/git/repositories/${project.repositoryId}/pullrequests/${prNum}/threads?api-version=7.1`;
215
+ const threadsData = await adoFetch(threadsUrl, token);
216
+ const threads = threadsData.value || [];
217
+
218
+ const cutoff = pr.humanFeedback?.lastProcessedCommentDate || pr.created || '1970-01-01';
219
+
220
+ // Collect ALL human comments on the PR for full context
221
+ const allHumanComments = [];
222
+ const newHumanComments = [];
223
+
224
+ for (const thread of threads) {
225
+ for (const comment of (thread.comments || [])) {
226
+ if (!comment.content || comment.commentType === 'system') continue;
227
+ if (/\bMinions\s*\(/i.test(comment.content)) continue; // skip minions's own comments
228
+
229
+ const entry = {
230
+ threadId: thread.id,
231
+ commentId: comment.id,
232
+ author: comment.author?.displayName || 'Human',
233
+ content: comment.content,
234
+ date: comment.publishedDate
235
+ };
236
+ allHumanComments.push(entry);
237
+
238
+ // Track which comments are new (for triggering — any new comment triggers a fix)
239
+ if (comment.publishedDate && comment.publishedDate > cutoff) {
240
+ newHumanComments.push(entry);
241
+ }
242
+ }
243
+ }
244
+
245
+ if (newHumanComments.length === 0) return false;
246
+
247
+ // Sort all comments chronologically and build full context for the fix agent
248
+ allHumanComments.sort((a, b) => a.date.localeCompare(b.date));
249
+ newHumanComments.sort((a, b) => a.date.localeCompare(b.date));
250
+ const latestDate = newHumanComments[newHumanComments.length - 1].date;
251
+
252
+ // Provide ALL comments as context — the agent needs full thread context to fix properly
253
+ const feedbackContent = allHumanComments
254
+ .map(c => {
255
+ const isNew = c.date > cutoff;
256
+ return `${isNew ? '**[NEW]** ' : ''}**${c.author}** (${c.date}):\n${c.content.replace(/@minions\s*/gi, '').trim()}`;
257
+ })
258
+ .join('\n\n---\n\n');
259
+
260
+ pr.humanFeedback = {
261
+ lastProcessedCommentDate: latestDate,
262
+ pendingFix: true,
263
+ feedbackContent
264
+ };
265
+
266
+ e.log('info', `PR ${pr.id}: ${newHumanComments.length} new comment(s), ${allHumanComments.length} total — full thread context provided`);
267
+ return true;
268
+ });
269
+
270
+ if (totalUpdated > 0) {
271
+ e.log('info', `PR comment poll: found human feedback on ${totalUpdated} PR(s)`);
272
+ }
273
+ }
274
+
275
+ // ─── PR Reconciliation Sweep ─────────────────────────────────────────────────
276
+
277
+ /**
278
+ * Reconcile PRs: find active ADO PRs created by the minions that aren't tracked
279
+ * in pull-requests.json, and add them. Matches PRs to work items by branch name.
280
+ */
281
+ async function reconcilePrs(config) {
282
+ const e = engine();
283
+ const token = getAdoToken();
284
+ if (!token) {
285
+ e.log('warn', 'Skipping PR reconciliation — no ADO token available');
286
+ return;
287
+ }
288
+
289
+ const projects = shared.getProjects(config);
290
+ const branchPatterns = [/^refs\/heads\/work\//i, /^refs\/heads\/feat\//i, /^refs\/heads\/user\/yemishin\//i];
291
+ let totalAdded = 0;
292
+
293
+ for (const project of projects) {
294
+ if (!project.adoOrg || !project.adoProject || !project.repositoryId) continue;
295
+
296
+ const orgBase = shared.getAdoOrgBase(project);
297
+ const url = `${orgBase}/${project.adoProject}/_apis/git/repositories/${project.repositoryId}/pullrequests?searchCriteria.status=active&api-version=7.1`;
298
+
299
+ let prData;
300
+ try {
301
+ prData = await adoFetch(url, token);
302
+ } catch (err) {
303
+ e.log('warn', `PR reconciliation failed for ${project.name}: ${err.message}`);
304
+ continue;
305
+ }
306
+
307
+ const adoPrs = (prData.value || []).filter(pr => {
308
+ const ref = pr.sourceRefName || '';
309
+ return branchPatterns.some(pat => pat.test(ref));
310
+ });
311
+
312
+ if (adoPrs.length === 0) continue;
313
+
314
+ const prPath = shared.projectPrPath(project);
315
+ const existingPrs = shared.safeJson(prPath) || [];
316
+ const existingIds = new Set(existingPrs.map(p => p.id));
317
+ let projectAdded = 0;
318
+
319
+ // Load work items to match branches to work item IDs
320
+ const wiPath = shared.projectWorkItemsPath(project);
321
+ const workItems = shared.safeJson(wiPath) || [];
322
+ const centralWiPath = require('path').join(shared.MINIONS_DIR, 'work-items.json');
323
+ const centralItems = shared.safeJson(centralWiPath) || [];
324
+ const allItems = [...workItems, ...centralItems];
325
+
326
+ let projectUpdated = 0;
327
+ for (const adoPr of adoPrs) {
328
+ const prId = `PR-${adoPr.pullRequestId}`;
329
+ const branch = (adoPr.sourceRefName || '').replace('refs/heads/', '');
330
+ const title = adoPr.title || '';
331
+ // Extract item ID from branch name or PR title (e.g., feat(P-2cafdc2a): ...)
332
+ const branchMatch = branch.match(/(P-[a-f0-9]{6,})/i) || branch.match(/(PL-W\d+)/i);
333
+ const titleMatch = title.match(/\((P-[a-f0-9]{6,})\)/) || title.match(/\((PL-W\d+)\)/);
334
+ const linkedItemId = branchMatch?.[1] || titleMatch?.[1] || null;
335
+ const linkedItem = linkedItemId ? allItems.find(i => i.id === linkedItemId) : null;
336
+ const confirmedItemId = linkedItem ? linkedItemId : null;
337
+
338
+ if (existingIds.has(prId)) {
339
+ // PR already tracked — write link to pr-links.json if we can extract an ID
340
+ if (confirmedItemId) {
341
+ addPrLink(prId, confirmedItemId);
342
+ projectUpdated++;
343
+ }
344
+ continue;
345
+ }
346
+
347
+ const prUrl = project.prUrlBase ? project.prUrlBase + adoPr.pullRequestId : '';
348
+ existingPrs.push({
349
+ id: prId,
350
+ title: (adoPr.title || `PR #${adoPr.pullRequestId}`).slice(0, 120),
351
+ agent: (adoPr.createdBy?.displayName || 'unknown').toLowerCase(),
352
+ branch,
353
+ reviewStatus: 'pending',
354
+ status: 'active',
355
+ created: (adoPr.creationDate || '').slice(0, 10) || e.dateStamp(),
356
+ url: prUrl,
357
+ });
358
+ if (confirmedItemId) addPrLink(prId, confirmedItemId);
359
+ existingIds.add(prId);
360
+ projectAdded++;
361
+ e.log('info', `PR reconciliation: added ${prId} (branch: ${branch}${confirmedItemId ? ', linked to ' + confirmedItemId : ''}) to ${project.name}`);
362
+ }
363
+
364
+ if (projectAdded > 0 || projectUpdated > 0) {
365
+ shared.safeWrite(prPath, existingPrs);
366
+ totalAdded += projectAdded;
367
+ if (projectUpdated > 0) e.log('info', `PR reconciliation: linked ${projectUpdated} existing PR(s) to PRD items in ${project.name}`);
368
+ }
369
+ }
370
+
371
+ if (totalAdded > 0) {
372
+ e.log('info', `PR reconciliation: added ${totalAdded} missing PR(s) across projects`);
373
+ }
374
+ }
375
+
376
+ module.exports = {
377
+ getAdoToken,
378
+ adoFetch,
379
+ pollPrStatus,
380
+ pollPrHumanComments,
381
+ reconcilePrs,
382
+ };
383
+
@@ -0,0 +1,23 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const dir = path.resolve(__dirname, '..');
4
+
5
+ console.log('=== Work Items (non-done) ===');
6
+ let items = [];
7
+ try { items = JSON.parse(fs.readFileSync(path.join(dir, 'work-items.json'), 'utf8')); } catch {}
8
+ items.filter(i => i.status !== 'done').forEach(i => {
9
+ console.log(i.id, (i.status || '').padEnd(12), (i.type || '').padEnd(12), (i.title || '').slice(0, 60));
10
+ });
11
+
12
+ console.log('\n=== Agent Status (derived from dispatch) ===');
13
+ const { getAgentStatus } = require('./queries');
14
+ for (const a of ['ripley', 'dallas', 'lambert', 'rebecca', 'ralph']) {
15
+ const s = getAgentStatus(a);
16
+ console.log(a.padEnd(10), s.status.padEnd(10), (s.task || '-').slice(0, 60));
17
+ }
18
+
19
+ console.log('\n=== Inbox ===');
20
+ try {
21
+ const inbox = fs.readdirSync(path.join(dir, 'notes', 'inbox')).filter(f => f.endsWith('.md'));
22
+ console.log(inbox.length + ' files:', inbox.join(', '));
23
+ } catch { console.log('empty'); }