@yemi33/minions 0.1.11 → 0.1.13

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 (44) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dashboard/js/command-center.js +377 -0
  3. package/dashboard/js/command-history.js +70 -0
  4. package/dashboard/js/command-input.js +268 -0
  5. package/dashboard/js/command-parser.js +129 -0
  6. package/dashboard/js/detail-panel.js +98 -0
  7. package/dashboard/js/live-stream.js +69 -0
  8. package/dashboard/js/modal-qa.js +268 -0
  9. package/dashboard/js/modal.js +131 -0
  10. package/dashboard/js/refresh.js +59 -0
  11. package/dashboard/js/render-agents.js +17 -0
  12. package/dashboard/js/render-dispatch.js +148 -0
  13. package/dashboard/js/render-inbox.js +126 -0
  14. package/dashboard/js/render-kb.js +107 -0
  15. package/dashboard/js/render-other.js +181 -0
  16. package/dashboard/js/render-plans.js +304 -0
  17. package/dashboard/js/render-prd.js +469 -0
  18. package/dashboard/js/render-prs.js +94 -0
  19. package/dashboard/js/render-schedules.js +158 -0
  20. package/dashboard/js/render-skills.js +89 -0
  21. package/dashboard/js/render-work-items.js +219 -0
  22. package/dashboard/js/settings.js +135 -0
  23. package/dashboard/js/state.js +84 -0
  24. package/dashboard/js/utils.js +39 -0
  25. package/dashboard/layout.html +123 -0
  26. package/dashboard/pages/engine.html +12 -0
  27. package/dashboard/pages/home.html +31 -0
  28. package/dashboard/pages/inbox.html +17 -0
  29. package/dashboard/pages/plans.html +4 -0
  30. package/dashboard/pages/prd.html +5 -0
  31. package/dashboard/pages/prs.html +4 -0
  32. package/dashboard/pages/schedule.html +10 -0
  33. package/dashboard/pages/work.html +5 -0
  34. package/dashboard/styles.css +598 -0
  35. package/dashboard-build.js +51 -0
  36. package/dashboard.html +179 -107
  37. package/dashboard.js +51 -1
  38. package/engine/ado.js +14 -0
  39. package/engine/cli.js +11 -0
  40. package/engine/github.js +14 -0
  41. package/engine/lifecycle.js +25 -29
  42. package/engine.js +106 -19
  43. package/package.json +1 -1
  44. package/routing.md +1 -1
package/engine.js CHANGED
@@ -166,6 +166,25 @@ function getRoutingTableCached() {
166
166
  return _routingCache;
167
167
  }
168
168
 
169
+ function getMonthlySpend(agentId) {
170
+ const metrics = safeJson(path.join(ENGINE_DIR, 'metrics.json')) || {};
171
+ const daily = metrics._daily || {};
172
+ const now = new Date();
173
+ const monthPrefix = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
174
+ let total = 0;
175
+ for (const [date, data] of Object.entries(daily)) {
176
+ if (date.startsWith(monthPrefix)) {
177
+ total += (data.perAgent?.[agentId]?.costUsd || 0);
178
+ }
179
+ }
180
+ // Fallback: if no per-agent daily data, use cumulative (less accurate for monthly)
181
+ if (total === 0 && metrics[agentId]?.totalCostUsd) {
182
+ // Can't distinguish monthly from cumulative — treat as monthly estimate
183
+ // This path is for backward compat before per-agent daily tracking was added
184
+ }
185
+ return total;
186
+ }
187
+
169
188
  function getAgentErrorRate(agentId) {
170
189
  const metricsPath = path.join(ENGINE_DIR, 'metrics.json');
171
190
  const metrics = safeJson(metricsPath) || {};
@@ -194,7 +213,15 @@ function resolveAgent(workType, config, authorAgent = null) {
194
213
  let preferred = route.preferred === '_author_' ? authorAgent : route.preferred;
195
214
  let fallback = route.fallback === '_author_' ? authorAgent : route.fallback;
196
215
 
197
- const isAvailable = (id) => agents[id] && isAgentIdle(id) && !_claimedAgents.has(id);
216
+ const isAvailable = (id) => {
217
+ if (!agents[id] || !isAgentIdle(id) || _claimedAgents.has(id)) return false;
218
+ // Budget check — no budget means infinite (no limit)
219
+ const budget = agents[id].monthlyBudgetUsd;
220
+ if (budget && budget > 0) {
221
+ if (getMonthlySpend(id) >= budget) return false;
222
+ }
223
+ return true;
224
+ };
198
225
 
199
226
  // Check preferred and fallback first (routing table order)
200
227
  if (preferred && isAvailable(preferred)) { _claimedAgents.add(preferred); return preferred; }
@@ -905,6 +932,20 @@ function spawnAgent(dispatchItem, config) {
905
932
  args.push('--allowedTools', claudeConfig.allowedTools);
906
933
  }
907
934
 
935
+ // Session resume: reuse last session if recent enough (< 2 hours)
936
+ if (!agentId.startsWith('temp-')) {
937
+ try {
938
+ const sessionFile = safeJson(path.join(AGENTS_DIR, agentId, 'session.json'));
939
+ if (sessionFile?.sessionId && sessionFile.savedAt) {
940
+ const sessionAge = Date.now() - new Date(sessionFile.savedAt).getTime();
941
+ if (sessionAge < 2 * 60 * 60 * 1000) { // 2 hour TTL
942
+ args.push('--resume', sessionFile.sessionId);
943
+ log('info', `Resuming session ${sessionFile.sessionId} for ${agentId} (age: ${Math.round(sessionAge / 60000)}min)`);
944
+ }
945
+ }
946
+ } catch {}
947
+ }
948
+
908
949
  // MCP servers: agents inherit from ~/.claude.json directly as Claude Code processes.
909
950
  // No --mcp-config needed — avoids redundant config and ensures agents always have latest servers.
910
951
 
@@ -1968,6 +2009,27 @@ function setCooldown(key) {
1968
2009
  saveCooldowns();
1969
2010
  }
1970
2011
 
2012
+ function setCooldownWithContext(key, context) {
2013
+ const existing = dispatchCooldowns.get(key);
2014
+ const pendingContexts = existing?.pendingContexts || [];
2015
+ if (context) pendingContexts.push(context);
2016
+ dispatchCooldowns.set(key, {
2017
+ timestamp: Date.now(),
2018
+ failures: existing?.failures || 0,
2019
+ pendingContexts
2020
+ });
2021
+ saveCooldowns();
2022
+ }
2023
+
2024
+ function getCoalescedContexts(key) {
2025
+ const entry = dispatchCooldowns.get(key);
2026
+ const contexts = entry?.pendingContexts || [];
2027
+ if (contexts.length > 0 && entry) {
2028
+ entry.pendingContexts = []; // Clear after retrieval
2029
+ }
2030
+ return contexts;
2031
+ }
2032
+
1971
2033
  function setCooldownFailure(key) {
1972
2034
  const existing = dispatchCooldowns.get(key);
1973
2035
  const failures = (existing?.failures || 0) + 1;
@@ -2390,19 +2452,16 @@ function discoverFromPrs(config, project) {
2390
2452
  if (activePrIds.has(pr.id)) continue; // Skip PRs with active dispatch (prevent race)
2391
2453
 
2392
2454
  const prNumber = (pr.id || '').replace(/^PR-/, '');
2393
- const minionsStatus = pr.minionsReview?.status;
2455
+ // Use reviewStatus as single source of truth (synced from ADO/GitHub votes)
2456
+ // minionsReview tracks metadata (reviewer, note) but not the authoritative status
2457
+ const reviewStatus = pr.reviewStatus || 'pending';
2394
2458
 
2395
- // PRs needing review
2396
- const needsReview = !minionsStatus || minionsStatus === 'waiting';
2459
+ // PRs needing review: pending or waiting (review dispatched but no verdict yet)
2460
+ const needsReview = reviewStatus === 'pending' || reviewStatus === 'waiting';
2397
2461
  if (needsReview) {
2398
2462
  const key = `review-${project?.name || 'default'}-${pr.id}`;
2399
2463
  if (isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) continue;
2400
- // No self-review: exclude the PR author from review assignment
2401
- const prAuthor = (pr.agent || '').toLowerCase();
2402
- let agentId = resolveAgent('review', config);
2403
- if (agentId && agentId === prAuthor) {
2404
- agentId = resolveAgent('review', config); // retry — prAuthor now claimed, gets skipped
2405
- }
2464
+ const agentId = resolveAgent('review', config);
2406
2465
  if (!agentId) continue;
2407
2466
 
2408
2467
  const item = buildPrDispatch(agentId, config, project, pr, 'review', {
@@ -2413,7 +2472,7 @@ function discoverFromPrs(config, project) {
2413
2472
  }
2414
2473
 
2415
2474
  // PRs with changes requested → route back to author for fix
2416
- if (minionsStatus === 'changes-requested') {
2475
+ if (reviewStatus === 'changes-requested') {
2417
2476
  const key = `fix-${project?.name || 'default'}-${pr.id}`;
2418
2477
  if (isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) continue;
2419
2478
  const agentId = resolveAgent('fix', config, pr.agent);
@@ -2426,22 +2485,37 @@ function discoverFromPrs(config, project) {
2426
2485
  if (item) { newWork.push(item); setCooldown(key); }
2427
2486
  }
2428
2487
 
2429
- // PRs with pending human feedback
2430
- if (pr.humanFeedback?.pendingFix) {
2431
- const key = `human-fix-${project?.name || 'default'}-${pr.id}`;
2432
- if (isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) continue;
2488
+ // PRs with pending human feedback (or coalesced comments from while agent was fixing)
2489
+ const humanFixKey = `human-fix-${project?.name || 'default'}-${pr.id}`;
2490
+ const hasCoalescedFeedback = (dispatchCooldowns.get(humanFixKey)?.pendingContexts || []).length > 0;
2491
+ if (pr.humanFeedback?.pendingFix || hasCoalescedFeedback) {
2492
+ const key = humanFixKey;
2493
+ if (isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) {
2494
+ // Coalesce: save feedback for next dispatch
2495
+ if (pr.humanFeedback?.feedbackContent) {
2496
+ setCooldownWithContext(key, { feedbackContent: pr.humanFeedback.feedbackContent, timestamp: new Date().toISOString() });
2497
+ }
2498
+ continue;
2499
+ }
2433
2500
  const agentId = resolveAgent('fix', config, pr.agent);
2434
2501
  if (!agentId) continue;
2435
2502
 
2503
+ const coalesced = getCoalescedContexts(key);
2504
+ let reviewNote = pr.humanFeedback.feedbackContent || 'See PR thread comments';
2505
+ if (coalesced.length > 0) {
2506
+ const earlier = coalesced.map(c => c.feedbackContent).filter(Boolean).join('\n\n---\n\n');
2507
+ if (earlier) reviewNote = earlier + '\n\n---\n\n' + reviewNote;
2508
+ }
2509
+
2436
2510
  const item = buildPrDispatch(agentId, config, project, pr, 'fix', {
2437
2511
  pr_id: pr.id, pr_number: prNumber, pr_title: pr.title || '', pr_branch: pr.branch || '',
2438
2512
  reviewer: 'Human Reviewer',
2439
- review_note: pr.humanFeedback.feedbackContent || 'See PR thread comments',
2513
+ review_note: reviewNote,
2440
2514
  }, `Fix PR ${pr.id} — human feedback`, { dispatchKey: key, source: 'pr-human-feedback', pr, branch: pr.branch, project: projMeta });
2441
2515
  if (item) { newWork.push(item); setCooldown(key); }
2442
2516
  }
2443
2517
 
2444
- // PRs with build failures
2518
+ // PRs with build failures — any agent can pick this up
2445
2519
  if (pr.status === 'active' && pr.buildStatus === 'failing') {
2446
2520
  const key = `build-fix-${project?.name || 'default'}-${pr.id}`;
2447
2521
  if (isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) continue;
@@ -2565,7 +2639,17 @@ function discoverFromWorkItems(config, project) {
2565
2639
  }
2566
2640
  const agentId = item.agent || resolveAgent(workType, config);
2567
2641
  if (!agentId) {
2568
- if (item._pendingReason !== 'no_agent') { item._pendingReason = 'no_agent'; needsWrite = true; }
2642
+ // Check if reason is budget
2643
+ const cfgAgents = config.agents || {};
2644
+ const budgetBlocked = Object.keys(cfgAgents).some(id => {
2645
+ const b = cfgAgents[id].monthlyBudgetUsd;
2646
+ return b && b > 0 && getMonthlySpend(id) >= b && isAgentIdle(id);
2647
+ });
2648
+ if (budgetBlocked) {
2649
+ if (item._pendingReason !== 'budget_exceeded') { item._pendingReason = 'budget_exceeded'; needsWrite = true; }
2650
+ } else {
2651
+ if (item._pendingReason !== 'no_agent') { item._pendingReason = 'no_agent'; needsWrite = true; }
2652
+ }
2569
2653
  skipped.noAgent++; continue;
2570
2654
  }
2571
2655
 
@@ -3396,7 +3480,10 @@ module.exports = {
3396
3480
  updateWorkItemStatus, runCleanup, handlePostMerge,
3397
3481
 
3398
3482
  // Cooldowns
3399
- loadCooldowns,
3483
+ loadCooldowns, setCooldownWithContext, getCoalescedContexts,
3484
+
3485
+ // Budget
3486
+ getMonthlySpend,
3400
3487
 
3401
3488
  // Tick
3402
3489
  tick,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
5
5
  "bin": {
6
6
  "minions": "bin/minions.js"
package/routing.md CHANGED
@@ -27,7 +27,7 @@ Notes:
27
27
  ## Rules
28
28
 
29
29
  1. **Eager by default** — spawn all agents who can start work, not one at a time
30
- 2. **No self-review** — author cannot review their own PR
30
+ 2. **Self-review is allowed** — agents can review their own PRs (useful for single-agent setups)
31
31
  3. **Exploration gates implementation** — when exploring, finish before implementing
32
32
  4. **Implementation informs PRD** — Lambert reads build summaries before writing PRD
33
33
  5. **All rules in `notes.md` apply** — engine injects them into every playbook