@yemi33/minions 0.1.11 → 0.1.12
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 +18 -0
- package/dashboard.html +179 -107
- package/dashboard.js +7 -0
- package/engine/ado.js +14 -0
- package/engine/cli.js +11 -0
- package/engine/github.js +14 -0
- package/engine/lifecycle.js +25 -29
- package/engine.js +106 -19
- package/package.json +1 -1
- package/routing.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.12 (2026-03-26)
|
|
4
|
+
|
|
5
|
+
### Engine
|
|
6
|
+
- engine.js
|
|
7
|
+
- engine/ado.js
|
|
8
|
+
- engine/cli.js
|
|
9
|
+
- engine/github.js
|
|
10
|
+
- engine/lifecycle.js
|
|
11
|
+
|
|
12
|
+
### Dashboard
|
|
13
|
+
- dashboard.html
|
|
14
|
+
- dashboard.js
|
|
15
|
+
|
|
16
|
+
### Other
|
|
17
|
+
- TODO.md
|
|
18
|
+
- routing.md
|
|
19
|
+
- test/unit.test.js
|
|
20
|
+
|
|
3
21
|
## 0.1.11 (2026-03-26)
|
|
4
22
|
|
|
5
23
|
### Engine
|
package/dashboard.html
CHANGED
|
@@ -45,9 +45,19 @@
|
|
|
45
45
|
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.3} }
|
|
46
46
|
.timestamp { color: var(--muted); font-size: var(--text-md); font-variant-numeric: tabular-nums; }
|
|
47
47
|
|
|
48
|
-
.layout { display:
|
|
48
|
+
.layout { display: none; } /* Replaced by page-layout */
|
|
49
49
|
section { padding: var(--space-8) var(--space-9); border-bottom: 1px solid var(--border); overflow: hidden; min-width: 0; }
|
|
50
|
-
|
|
50
|
+
|
|
51
|
+
/* Sidebar navigation */
|
|
52
|
+
.page-layout { display: flex; height: calc(100vh - 44px); overflow: hidden; }
|
|
53
|
+
.sidebar { width: 150px; min-width: 150px; background: var(--surface); border-right: 1px solid var(--border); padding: var(--space-4) 0; overflow-y: auto; position: sticky; top: 0; }
|
|
54
|
+
.sidebar-link { display: flex; align-items: center; justify-content: space-between; padding: 8px 14px; color: var(--muted); text-decoration: none; font-size: var(--text-sm); border-left: 3px solid transparent; transition: all var(--transition-fast); cursor: pointer; }
|
|
55
|
+
.sidebar-link:hover { color: var(--text); background: var(--surface2); }
|
|
56
|
+
.sidebar-link.active { color: var(--blue); border-left-color: var(--blue); background: var(--surface2); font-weight: 600; }
|
|
57
|
+
.sidebar-count { font-size: 9px; color: var(--muted); background: var(--surface2); padding: 1px 5px; border-radius: 8px; min-width: 16px; text-align: center; }
|
|
58
|
+
.page-content { flex: 1; overflow-y: auto; min-width: 0; }
|
|
59
|
+
.page { display: none; }
|
|
60
|
+
.page.active { display: block; }
|
|
51
61
|
section h2 { font-size: var(--text-base); font-weight: 600; text-transform: uppercase; letter-spacing: 1px; color: var(--muted); margin-bottom: 14px; display: flex; align-items: center; gap: var(--space-4); }
|
|
52
62
|
section h2 .count { background: var(--surface2); border: 1px solid var(--border); border-radius: var(--radius-xl); padding: var(--space-1) 7px; font-size: var(--text-base); color: var(--text); }
|
|
53
63
|
|
|
@@ -189,7 +199,7 @@
|
|
|
189
199
|
.inbox-name { font-weight: 500; font-size: var(--text-md); color: var(--purple); margin-bottom: var(--space-2); display: flex; justify-content: space-between; }
|
|
190
200
|
.inbox-preview { font-size: var(--text-base); color: var(--muted); line-height: 1.5; max-height: 60px; overflow: hidden; }
|
|
191
201
|
|
|
192
|
-
.prd-panel, .pr-panel {
|
|
202
|
+
.prd-panel, .pr-panel { border-bottom: 1px solid var(--border); overflow: visible; min-width: 0; }
|
|
193
203
|
.prd-inner { display: flex; gap: 16px; align-items: flex-start; }
|
|
194
204
|
.prd-stats { display: flex; gap: 16px; }
|
|
195
205
|
.prd-stat { text-align: center; background: var(--surface2); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: var(--space-6) var(--space-8); }
|
|
@@ -331,7 +341,7 @@
|
|
|
331
341
|
.empty { color: var(--muted); font-style: italic; font-size: var(--text-md); padding: var(--space-4) 0; }
|
|
332
342
|
|
|
333
343
|
/* Command Center — Unified Input */
|
|
334
|
-
.cmd-center {
|
|
344
|
+
.cmd-center { overflow: visible !important; }
|
|
335
345
|
.cmd-input-wrap {
|
|
336
346
|
position: relative; display: flex; align-items: flex-start; gap: 0;
|
|
337
347
|
background: var(--bg); border: 2px solid var(--border); border-radius: var(--radius-xl);
|
|
@@ -642,111 +652,134 @@
|
|
|
642
652
|
<code style="background:var(--bg);padding:6px 16px;border-radius:4px;font-size:14px;color:var(--blue);border:1px solid var(--border)">minions init</code>
|
|
643
653
|
</div>
|
|
644
654
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
655
|
+
<!-- Layout replaced by page-layout sidebar navigation -->
|
|
656
|
+
|
|
657
|
+
<div class="page-layout">
|
|
658
|
+
<nav class="sidebar" id="sidebar">
|
|
659
|
+
<a class="sidebar-link" data-page="home" href="/">Home</a>
|
|
660
|
+
<a class="sidebar-link" data-page="work" href="/work">Work Items <span class="sidebar-count" id="sidebar-wi"></span></a>
|
|
661
|
+
<a class="sidebar-link" data-page="prd" href="/prd">PRD</a>
|
|
662
|
+
<a class="sidebar-link" data-page="prs" href="/prs">Pull Requests <span class="sidebar-count" id="sidebar-pr"></span></a>
|
|
663
|
+
<a class="sidebar-link" data-page="plans" href="/plans">Plans</a>
|
|
664
|
+
<a class="sidebar-link" data-page="inbox" href="/inbox">Notes & KB</a>
|
|
665
|
+
<a class="sidebar-link" data-page="schedule" href="/schedule">Schedules</a>
|
|
666
|
+
<a class="sidebar-link" data-page="engine" href="/engine">Engine</a>
|
|
667
|
+
</nav>
|
|
668
|
+
<div class="page-content" id="page-content">
|
|
669
|
+
|
|
670
|
+
<div class="page active" id="page-home">
|
|
671
|
+
<section class="cmd-center">
|
|
672
|
+
<h2>Command Center</h2>
|
|
673
|
+
<div class="cmd-input-wrap" id="cmd-input-wrap">
|
|
674
|
+
<div class="cmd-highlight-layer" id="cmd-highlight" aria-hidden="true"></div>
|
|
675
|
+
<textarea id="cmd-input" rows="1" placeholder='What do you need? e.g. "Fix the auth bug @dallas", "explain the dispatch flow", or "/note always use feature flags"'
|
|
676
|
+
oninput="cmdInputChanged()" onkeydown="cmdKeyDown(event)" onscroll="syncHighlightScroll()"></textarea>
|
|
677
|
+
<button class="cmd-send-btn" id="cmd-send-btn" onclick="cmdSubmit()">Send <kbd>Ctrl+Enter</kbd></button>
|
|
678
|
+
</div>
|
|
679
|
+
<div class="cmd-mention-popup" id="cmd-mention-popup"></div>
|
|
680
|
+
<div class="cmd-meta" id="cmd-meta" style="display:none"></div>
|
|
681
|
+
<div class="cmd-hints">
|
|
682
|
+
<span style="color:var(--blue);font-weight:600">Command Center</span>
|
|
683
|
+
<span>Ask anything, dispatch work, manage plans — powered by Sonnet</span>
|
|
684
|
+
<button class="cmd-history-btn" onclick="cmdShowHistory()">Past Commands</button>
|
|
685
|
+
</div>
|
|
686
|
+
<div class="cmd-toast" id="cmd-toast"></div>
|
|
687
|
+
</section>
|
|
688
|
+
<section>
|
|
689
|
+
<h2>Minions Members <span style="font-size:10px;color:var(--border);font-weight:400;text-transform:none;letter-spacing:0">click for details</span></h2>
|
|
690
|
+
<div class="agents" id="agents-grid">Loading...</div>
|
|
691
|
+
</section>
|
|
692
|
+
<section>
|
|
693
|
+
<h2>Dispatch Queue</h2>
|
|
694
|
+
<div class="dispatch-stats" id="dispatch-stats"></div>
|
|
695
|
+
<div id="dispatch-active"></div>
|
|
696
|
+
<div id="dispatch-pending"></div>
|
|
697
|
+
</section>
|
|
698
|
+
<section class="pr-panel" id="completed-section">
|
|
699
|
+
<h2>Recent Completions <span class="count" id="completed-count">0</span></h2>
|
|
700
|
+
<div id="completed-content"><p class="empty">No completed dispatches yet.</p></div>
|
|
701
|
+
</section>
|
|
702
|
+
</div>
|
|
703
|
+
|
|
704
|
+
<div class="page" id="page-work">
|
|
705
|
+
<section id="work-items-section" style="overflow:visible">
|
|
706
|
+
<h2>Work Items <span class="count" id="wi-count">0</span> <button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;margin-left:8px" onclick="toggleWorkItemArchive()">See Archive</button></h2>
|
|
707
|
+
<div id="work-items-content"><p class="empty">No work items. Add tasks via Command Center above.</p></div>
|
|
708
|
+
<div id="work-items-archive" style="display:none;margin-top:12px"></div>
|
|
709
|
+
</section>
|
|
710
|
+
</div>
|
|
711
|
+
|
|
712
|
+
<div class="page" id="page-prd">
|
|
713
|
+
<section class="prd-panel" id="prd-section">
|
|
714
|
+
<h2>PRD <span class="count" id="prd-progress-count">0%</span> <span id="prd-badge"></span> <span id="archive-btns"></span></h2>
|
|
715
|
+
<div id="prd-content"><p class="prd-pending">No PRD found.</p></div>
|
|
716
|
+
<div id="prd-progress-content" style="margin-top:12px"></div>
|
|
717
|
+
</section>
|
|
718
|
+
</div>
|
|
719
|
+
|
|
720
|
+
<div class="page" id="page-prs">
|
|
721
|
+
<section class="pr-panel" id="pr-section">
|
|
722
|
+
<h2>Pull Requests <span class="count" id="pr-count">0</span></h2>
|
|
723
|
+
<div id="pr-content"><p class="pr-empty">No pull requests yet.</p></div>
|
|
724
|
+
</section>
|
|
725
|
+
</div>
|
|
726
|
+
|
|
727
|
+
<div class="page" id="page-plans">
|
|
728
|
+
<section>
|
|
729
|
+
<h2>Plans <span class="count" id="plans-count">0</span></h2>
|
|
730
|
+
<div id="plans-list"><p class="empty">No plans yet. Use /plan in the command center to create one.</p></div>
|
|
731
|
+
</section>
|
|
732
|
+
</div>
|
|
733
|
+
|
|
734
|
+
<div class="page" id="page-inbox">
|
|
735
|
+
<section>
|
|
736
|
+
<h2>Notes Inbox <span class="count" id="inbox-count">0</span> <span style="font-size:10px;color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">auto-consolidates at 3 notes</span></h2>
|
|
737
|
+
<div class="inbox-list" id="inbox-list">Loading...</div>
|
|
738
|
+
</section>
|
|
739
|
+
<section>
|
|
740
|
+
<h2 data-file="notes.md" style="position:relative">Team Notes <span style="font-size:10px;color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0" id="notes-updated"></span></h2>
|
|
741
|
+
<div id="notes-list">Loading...</div>
|
|
742
|
+
</section>
|
|
743
|
+
<section>
|
|
744
|
+
<h2>Knowledge Base <span class="count" id="kb-count">0</span> <button id="kb-sweep-btn" onclick="kbSweep()" style="font-size:9px;padding:2px 8px;background:var(--surface2);border:1px solid var(--border);color:var(--muted);border-radius:4px;cursor:pointer;margin-left:8px;vertical-align:middle">sweep</button><span id="kb-swept-time" style="font-size:10px;color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0;margin-left:8px"></span></h2>
|
|
745
|
+
<div class="kb-tabs" id="kb-tabs"></div>
|
|
746
|
+
<div class="kb-list" id="kb-list"><p class="empty">No knowledge entries yet. Notes are classified here after consolidation.</p></div>
|
|
747
|
+
</section>
|
|
748
|
+
<section>
|
|
749
|
+
<h2>Minions Skills <span class="count" id="skills-count">0</span></h2>
|
|
750
|
+
<div id="skills-list"><p class="empty">No skills yet. Agents create these when they discover repeatable workflows.</p></div>
|
|
751
|
+
</section>
|
|
752
|
+
</div>
|
|
753
|
+
|
|
754
|
+
<div class="page" id="page-schedule">
|
|
755
|
+
<section id="scheduled-section">
|
|
756
|
+
<h2>Scheduled Tasks <span class="count" id="scheduled-count">0</span>
|
|
757
|
+
<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--green);border-color:var(--green);margin-left:8px" onclick="openCreateScheduleModal()">+ New</button>
|
|
758
|
+
</h2>
|
|
759
|
+
<div id="scheduled-content"><p class="empty">No scheduled tasks. Add one to automate recurring work.</p></div>
|
|
760
|
+
</section>
|
|
761
|
+
<section>
|
|
762
|
+
<h2>MCP Servers <span class="count" id="mcp-count">0</span></h2>
|
|
763
|
+
<div id="mcp-list"><p class="empty">No MCP servers synced.</p></div>
|
|
764
|
+
</section>
|
|
653
765
|
</div>
|
|
654
|
-
|
|
655
|
-
<div class="
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
766
|
+
|
|
767
|
+
<div class="page" id="page-engine">
|
|
768
|
+
<section>
|
|
769
|
+
<h2>Engine Log</h2>
|
|
770
|
+
<div class="log-list" id="engine-log">No log entries yet.</div>
|
|
771
|
+
</section>
|
|
772
|
+
<section>
|
|
773
|
+
<h2>Agent Metrics</h2>
|
|
774
|
+
<div id="metrics-content"><p class="empty">No metrics yet. Metrics appear after agents complete tasks.</p></div>
|
|
775
|
+
</section>
|
|
776
|
+
<section>
|
|
777
|
+
<h2>Token Usage</h2>
|
|
778
|
+
<div id="token-usage-content"><p class="empty">No usage data yet.</p></div>
|
|
779
|
+
</section>
|
|
660
780
|
</div>
|
|
661
|
-
|
|
662
|
-
</
|
|
663
|
-
|
|
664
|
-
<section>
|
|
665
|
-
<h2>Minions Members <span style="font-size:10px;color:var(--border);font-weight:400;text-transform:none;letter-spacing:0">click for details</span></h2>
|
|
666
|
-
<div class="agents" id="agents-grid">Loading...</div>
|
|
667
|
-
</section>
|
|
668
|
-
|
|
669
|
-
<section id="work-items-section" style="overflow:visible">
|
|
670
|
-
<h2>Work Items <span class="count" id="wi-count">0</span> <button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;margin-left:8px" onclick="toggleWorkItemArchive()">See Archive</button></h2>
|
|
671
|
-
<div id="work-items-content"><p class="empty">No work items. Add tasks via Command Center above.</p></div>
|
|
672
|
-
<div id="work-items-archive" style="display:none;margin-top:12px"></div>
|
|
673
|
-
</section>
|
|
674
|
-
|
|
675
|
-
<section class="prd-panel" id="prd-section">
|
|
676
|
-
<h2>PRD <span class="count" id="prd-progress-count">0%</span> <span id="prd-badge"></span> <span id="archive-btns"></span></h2>
|
|
677
|
-
<div id="prd-content"><p class="prd-pending">No PRD found.</p></div>
|
|
678
|
-
<div id="prd-progress-content" style="margin-top:12px"></div>
|
|
679
|
-
</section>
|
|
680
|
-
|
|
681
|
-
<section class="pr-panel" id="pr-section">
|
|
682
|
-
<h2>Pull Requests <span class="count" id="pr-count">0</span></h2>
|
|
683
|
-
<div id="pr-content"><p class="pr-empty">No pull requests yet.</p></div>
|
|
684
|
-
</section>
|
|
685
|
-
|
|
686
|
-
<section>
|
|
687
|
-
<h2>Plans <span class="count" id="plans-count">0</span></h2>
|
|
688
|
-
<div id="plans-list"><p class="empty">No plans yet. Use /plan in the command center to create one.</p></div>
|
|
689
|
-
</section>
|
|
690
|
-
|
|
691
|
-
<section>
|
|
692
|
-
<h2>Notes Inbox <span class="count" id="inbox-count">0</span> <span style="font-size:10px;color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">auto-consolidates at 3 notes</span></h2>
|
|
693
|
-
<div class="inbox-list" id="inbox-list">Loading...</div>
|
|
694
|
-
</section>
|
|
695
|
-
|
|
696
|
-
<section>
|
|
697
|
-
<h2 data-file="notes.md" style="position:relative">Team Notes <span style="font-size:10px;color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0" id="notes-updated"></span></h2>
|
|
698
|
-
<div id="notes-list">Loading...</div>
|
|
699
|
-
</section>
|
|
700
|
-
|
|
701
|
-
<section>
|
|
702
|
-
<h2>Knowledge Base <span class="count" id="kb-count">0</span> <button id="kb-sweep-btn" onclick="kbSweep()" style="font-size:9px;padding:2px 8px;background:var(--surface2);border:1px solid var(--border);color:var(--muted);border-radius:4px;cursor:pointer;margin-left:8px;vertical-align:middle">sweep</button><span id="kb-swept-time" style="font-size:10px;color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0;margin-left:8px"></span></h2>
|
|
703
|
-
<div class="kb-tabs" id="kb-tabs"></div>
|
|
704
|
-
<div class="kb-list" id="kb-list"><p class="empty">No knowledge entries yet. Notes are classified here after consolidation.</p></div>
|
|
705
|
-
</section>
|
|
706
|
-
|
|
707
|
-
<section>
|
|
708
|
-
<h2>Minions Skills <span class="count" id="skills-count">0</span></h2>
|
|
709
|
-
<div id="skills-list"><p class="empty">No skills yet. Agents create these when they discover repeatable workflows.</p></div>
|
|
710
|
-
</section>
|
|
711
|
-
|
|
712
|
-
<section>
|
|
713
|
-
<h2>MCP Servers <span class="count" id="mcp-count">0</span></h2>
|
|
714
|
-
<div id="mcp-list"><p class="empty">No MCP servers synced.</p></div>
|
|
715
|
-
</section>
|
|
716
|
-
|
|
717
|
-
<section id="scheduled-section">
|
|
718
|
-
<h2>Scheduled Tasks <span class="count" id="scheduled-count">0</span>
|
|
719
|
-
<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--green);border-color:var(--green);margin-left:8px" onclick="openCreateScheduleModal()">+ New</button>
|
|
720
|
-
</h2>
|
|
721
|
-
<div id="scheduled-content"><p class="empty">No scheduled tasks. Add one to automate recurring work.</p></div>
|
|
722
|
-
</section>
|
|
723
|
-
|
|
724
|
-
<section>
|
|
725
|
-
<h2>Dispatch Queue</h2>
|
|
726
|
-
<div class="dispatch-stats" id="dispatch-stats"></div>
|
|
727
|
-
<div id="dispatch-active"></div>
|
|
728
|
-
<div id="dispatch-pending"></div>
|
|
729
|
-
</section>
|
|
730
|
-
|
|
731
|
-
<section>
|
|
732
|
-
<h2>Engine Log</h2>
|
|
733
|
-
<div class="log-list" id="engine-log">No log entries yet.</div>
|
|
734
|
-
</section>
|
|
735
|
-
|
|
736
|
-
<section>
|
|
737
|
-
<h2>Agent Metrics</h2>
|
|
738
|
-
<div id="metrics-content"><p class="empty">No metrics yet. Metrics appear after agents complete tasks.</p></div>
|
|
739
|
-
</section>
|
|
740
|
-
|
|
741
|
-
<section>
|
|
742
|
-
<h2>Token Usage</h2>
|
|
743
|
-
<div id="token-usage-content"><p class="empty">No usage data yet.</p></div>
|
|
744
|
-
</section>
|
|
745
|
-
|
|
746
|
-
<section class="pr-panel" id="completed-section">
|
|
747
|
-
<h2>Recent Completions <span class="count" id="completed-count">0</span></h2>
|
|
748
|
-
<div id="completed-content"><p class="empty">No completed dispatches yet.</p></div>
|
|
749
|
-
</section>
|
|
781
|
+
|
|
782
|
+
</div>
|
|
750
783
|
</div>
|
|
751
784
|
|
|
752
785
|
<!-- Agent Detail Panel -->
|
|
@@ -803,6 +836,34 @@ let inboxData = [];
|
|
|
803
836
|
let agentData = [];
|
|
804
837
|
let currentAgentId = null;
|
|
805
838
|
let currentTab = 'thought-process';
|
|
839
|
+
|
|
840
|
+
// Sidebar page navigation — URL-routed
|
|
841
|
+
function getPageFromUrl() {
|
|
842
|
+
const path = window.location.pathname.replace(/^\//, '') || 'home';
|
|
843
|
+
if (document.querySelector('.sidebar-link[data-page="' + path + '"]')) return path;
|
|
844
|
+
return 'home';
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
let currentPage = getPageFromUrl();
|
|
848
|
+
|
|
849
|
+
function switchPage(page, pushState) {
|
|
850
|
+
currentPage = page;
|
|
851
|
+
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
|
852
|
+
const target = document.getElementById('page-' + page);
|
|
853
|
+
if (target) target.classList.add('active');
|
|
854
|
+
document.querySelectorAll('.sidebar-link').forEach(l => l.classList.remove('active'));
|
|
855
|
+
const link = document.querySelector('.sidebar-link[data-page="' + page + '"]');
|
|
856
|
+
if (link) link.classList.add('active');
|
|
857
|
+
if (pushState !== false) {
|
|
858
|
+
const url = page === 'home' ? '/' : '/' + page;
|
|
859
|
+
history.pushState({ page }, '', url);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Browser back/forward navigation
|
|
864
|
+
window.addEventListener('popstate', (e) => {
|
|
865
|
+
switchPage(e.state?.page || getPageFromUrl(), false);
|
|
866
|
+
});
|
|
806
867
|
window._prdRequeueUi = window._prdRequeueUi || {};
|
|
807
868
|
|
|
808
869
|
function getPrdRequeueState(workItemId) {
|
|
@@ -2388,6 +2449,11 @@ async function refresh() {
|
|
|
2388
2449
|
renderSkills(data.skills || []);
|
|
2389
2450
|
renderMcpServers(data.mcpServers || []);
|
|
2390
2451
|
renderSchedules(data.schedules || []);
|
|
2452
|
+
// Update sidebar counts
|
|
2453
|
+
const swi = document.getElementById('sidebar-wi');
|
|
2454
|
+
if (swi) swi.textContent = (data.workItems || []).length || '';
|
|
2455
|
+
const spr = document.getElementById('sidebar-pr');
|
|
2456
|
+
if (spr) spr.textContent = (data.pullRequests || []).length || '';
|
|
2391
2457
|
// Refresh KB and plans less frequently (every 3rd cycle = ~12s)
|
|
2392
2458
|
if (!window._kbRefreshCount) window._kbRefreshCount = 0;
|
|
2393
2459
|
if (window._kbRefreshCount++ % 3 === 0) { refreshKnowledgeBase(); refreshPlans(); }
|
|
@@ -2397,6 +2463,12 @@ async function refresh() {
|
|
|
2397
2463
|
refresh();
|
|
2398
2464
|
setInterval(refresh, 4000);
|
|
2399
2465
|
|
|
2466
|
+
// Wire sidebar navigation
|
|
2467
|
+
document.querySelectorAll('.sidebar-link').forEach(link => {
|
|
2468
|
+
link.addEventListener('click', e => { e.preventDefault(); switchPage(link.dataset.page); });
|
|
2469
|
+
});
|
|
2470
|
+
switchPage(currentPage);
|
|
2471
|
+
|
|
2400
2472
|
// -- Projects --
|
|
2401
2473
|
function renderProjects(projects) {
|
|
2402
2474
|
const header = document.getElementById('header-projects');
|
package/dashboard.js
CHANGED
|
@@ -2767,6 +2767,13 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2767
2767
|
{ method: 'POST', path: '/api/schedules/delete', desc: 'Delete a schedule', params: 'id', handler: handleSchedulesDelete },
|
|
2768
2768
|
|
|
2769
2769
|
// Engine
|
|
2770
|
+
{ method: 'POST', path: '/api/engine/wakeup', desc: 'Trigger immediate engine tick via control.json signal', handler: async (req, res) => {
|
|
2771
|
+
const controlPath = path.join(MINIONS_DIR, 'engine', 'control.json');
|
|
2772
|
+
const control = shared.safeJson(controlPath) || {};
|
|
2773
|
+
control._wakeupAt = Date.now();
|
|
2774
|
+
shared.safeWrite(controlPath, control);
|
|
2775
|
+
return jsonReply(res, 200, { ok: true, message: 'Wakeup signal sent' });
|
|
2776
|
+
}},
|
|
2770
2777
|
{ method: 'POST', path: '/api/engine/restart', desc: 'Force-kill engine and restart immediately', handler: handleEngineRestart },
|
|
2771
2778
|
|
|
2772
2779
|
// Settings
|
package/engine/ado.js
CHANGED
|
@@ -149,6 +149,20 @@ async function pollPrStatus(config) {
|
|
|
149
149
|
e.log('info', `PR ${pr.id} reviewStatus: ${pr.reviewStatus} → ${newReviewStatus}`);
|
|
150
150
|
pr.reviewStatus = newReviewStatus;
|
|
151
151
|
updated = true;
|
|
152
|
+
// Update author metrics when verdict changes to approved/rejected
|
|
153
|
+
if (newReviewStatus === 'approved' || newReviewStatus === 'changes-requested') {
|
|
154
|
+
const authorId = (pr.agent || '').toLowerCase();
|
|
155
|
+
if (authorId) {
|
|
156
|
+
try {
|
|
157
|
+
const metricsPath = path.join(__dirname, 'metrics.json');
|
|
158
|
+
const metrics = shared.safeJson(metricsPath) || {};
|
|
159
|
+
if (!metrics[authorId]) metrics[authorId] = {};
|
|
160
|
+
if (newReviewStatus === 'approved') metrics[authorId].prsApproved = (metrics[authorId].prsApproved || 0) + 1;
|
|
161
|
+
else metrics[authorId].prsRejected = (metrics[authorId].prsRejected || 0) + 1;
|
|
162
|
+
shared.safeWrite(metricsPath, metrics);
|
|
163
|
+
} catch {}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
152
166
|
}
|
|
153
167
|
|
|
154
168
|
if (newStatus !== 'active') return updated;
|
package/engine/cli.js
CHANGED
|
@@ -306,6 +306,17 @@ const commands = {
|
|
|
306
306
|
|
|
307
307
|
// Start tick loop
|
|
308
308
|
const tickTimer = setInterval(() => e.tick(), interval);
|
|
309
|
+
|
|
310
|
+
// Fast poll for immediate wakeup signals (checks control.json every 2s)
|
|
311
|
+
setInterval(() => {
|
|
312
|
+
const ctrl = getControl();
|
|
313
|
+
if (ctrl._wakeupAt && Date.now() - ctrl._wakeupAt < 5000) {
|
|
314
|
+
delete ctrl._wakeupAt;
|
|
315
|
+
safeWrite(CONTROL_PATH, ctrl);
|
|
316
|
+
e.tick();
|
|
317
|
+
}
|
|
318
|
+
}, 2000);
|
|
319
|
+
|
|
309
320
|
console.log(`Tick interval: ${interval / 1000}s | Max concurrent: ${config.engine?.maxConcurrent || 5}`);
|
|
310
321
|
console.log('Press Ctrl+C to stop');
|
|
311
322
|
|
package/engine/github.js
CHANGED
|
@@ -127,6 +127,20 @@ async function pollPrStatus(config) {
|
|
|
127
127
|
e.log('info', `PR ${pr.id} reviewStatus: ${pr.reviewStatus} → ${newReviewStatus}`);
|
|
128
128
|
pr.reviewStatus = newReviewStatus;
|
|
129
129
|
updated = true;
|
|
130
|
+
// Update author metrics when verdict changes to approved/rejected
|
|
131
|
+
if (newReviewStatus === 'approved' || newReviewStatus === 'changes-requested') {
|
|
132
|
+
const authorId = (pr.agent || '').toLowerCase();
|
|
133
|
+
if (authorId) {
|
|
134
|
+
try {
|
|
135
|
+
const metricsPath = path.join(__dirname, 'metrics.json');
|
|
136
|
+
const metrics = shared.safeJson(metricsPath) || {};
|
|
137
|
+
if (!metrics[authorId]) metrics[authorId] = {};
|
|
138
|
+
if (newReviewStatus === 'approved') metrics[authorId].prsApproved = (metrics[authorId].prsApproved || 0) + 1;
|
|
139
|
+
else metrics[authorId].prsRejected = (metrics[authorId].prsRejected || 0) + 1;
|
|
140
|
+
shared.safeWrite(metricsPath, metrics);
|
|
141
|
+
} catch {}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
130
144
|
}
|
|
131
145
|
}
|
|
132
146
|
|
package/engine/lifecycle.js
CHANGED
|
@@ -617,28 +617,25 @@ function updatePrAfterReview(agentId, pr, project) {
|
|
|
617
617
|
const dispatch = getDispatch();
|
|
618
618
|
const completedEntry = (dispatch.completed || []).find(d => d.agent === agentId && d.type === 'review');
|
|
619
619
|
|
|
620
|
+
// Set reviewStatus to 'waiting' (single source of truth — synced from ADO/GitHub votes on next poll)
|
|
621
|
+
target.reviewStatus = 'waiting';
|
|
620
622
|
target.minionsReview = {
|
|
621
|
-
status: 'waiting',
|
|
622
623
|
reviewer: reviewerName,
|
|
623
624
|
reviewedAt: e.ts(),
|
|
624
625
|
note: completedEntry?.task || ''
|
|
625
626
|
};
|
|
626
|
-
|
|
627
|
+
// Metrics update: don't track 'waiting' as a verdict — metrics are updated
|
|
628
|
+
// when pollPrStatus syncs the actual vote to minionsReview.status.
|
|
629
|
+
// The reviewer's reviewsDone counter is incremented in the main updateMetrics call.
|
|
627
630
|
|
|
631
|
+
// Track reviewer for metrics purposes
|
|
628
632
|
const authorAgentId = (pr.agent || '').toLowerCase();
|
|
629
633
|
if (authorAgentId && config.agents?.[authorAgentId]) {
|
|
630
634
|
const metricsPath = path.join(ENGINE_DIR, 'metrics.json');
|
|
631
635
|
const metrics = safeJson(metricsPath) || {};
|
|
632
636
|
if (!metrics[authorAgentId]) metrics[authorAgentId] = { tasksCompleted:0, tasksErrored:0, prsCreated:0, prsApproved:0, prsRejected:0, reviewsDone:0, lastTask:null, lastCompleted:null };
|
|
633
|
-
if (!metrics[
|
|
634
|
-
|
|
635
|
-
if (prevVerdict !== minionsVerdict) {
|
|
636
|
-
if (prevVerdict === 'approved') metrics[authorAgentId].prsApproved = Math.max(0, (metrics[authorAgentId].prsApproved || 0) - 1);
|
|
637
|
-
else if (prevVerdict === 'changes-requested') metrics[authorAgentId].prsRejected = Math.max(0, (metrics[authorAgentId].prsRejected || 0) - 1);
|
|
638
|
-
if (minionsVerdict === 'approved') metrics[authorAgentId].prsApproved++;
|
|
639
|
-
else if (minionsVerdict === 'changes-requested') metrics[authorAgentId].prsRejected++;
|
|
640
|
-
metrics[authorAgentId]._reviewedPrs[pr.id] = minionsVerdict;
|
|
641
|
-
}
|
|
637
|
+
if (!metrics[agentId]) metrics[agentId] = { tasksCompleted:0, tasksErrored:0, prsCreated:0, prsApproved:0, prsRejected:0, reviewsDone:0, lastTask:null, lastCompleted:null };
|
|
638
|
+
metrics[agentId].reviewsDone = (metrics[agentId].reviewsDone || 0) + 1;
|
|
642
639
|
shared.safeWrite(metricsPath, metrics);
|
|
643
640
|
}
|
|
644
641
|
|
|
@@ -654,25 +651,15 @@ function updatePrAfterFix(pr, project, source) {
|
|
|
654
651
|
const target = prs.find(p => p.id === pr.id);
|
|
655
652
|
if (!target) return;
|
|
656
653
|
|
|
654
|
+
// Reset reviewStatus to 'waiting' for re-review (single source of truth)
|
|
655
|
+
target.reviewStatus = 'waiting';
|
|
657
656
|
if (source === 'pr-human-feedback') {
|
|
658
|
-
// Human feedback fix: clear pendingFix AND reset to waiting for re-review
|
|
659
657
|
if (target.humanFeedback) target.humanFeedback.pendingFix = false;
|
|
660
|
-
target.minionsReview = {
|
|
661
|
-
...target.minionsReview,
|
|
662
|
-
status: 'waiting',
|
|
663
|
-
note: 'Fixed human feedback, awaiting re-review',
|
|
664
|
-
fixedAt: e.ts()
|
|
665
|
-
};
|
|
658
|
+
target.minionsReview = { ...target.minionsReview, note: 'Fixed human feedback, awaiting re-review', fixedAt: e.ts() };
|
|
666
659
|
e.log('info', `Updated ${pr.id} → cleared humanFeedback.pendingFix, reset to waiting for re-review`);
|
|
667
660
|
} else {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
...target.minionsReview,
|
|
671
|
-
status: 'waiting',
|
|
672
|
-
note: 'Fixed, awaiting re-review',
|
|
673
|
-
fixedAt: e.ts()
|
|
674
|
-
};
|
|
675
|
-
e.log('info', `Updated ${pr.id} → minions review: waiting (fix pushed)`);
|
|
661
|
+
target.minionsReview = { ...target.minionsReview, note: 'Fixed, awaiting re-review', fixedAt: e.ts() };
|
|
662
|
+
e.log('info', `Updated ${pr.id} → reviewStatus: waiting (fix pushed)`);
|
|
676
663
|
}
|
|
677
664
|
|
|
678
665
|
shared.safeWrite(project ? shared.projectPrPath(project) : path.join(path.resolve(MINIONS_DIR, '..'), '.minions', 'pull-requests.json'), prs);
|
|
@@ -932,8 +919,8 @@ function updateMetrics(agentId, dispatchItem, result, taskUsage, prsCreatedCount
|
|
|
932
919
|
// ─── Agent Output Parsing ────────────────────────────────────────────────────
|
|
933
920
|
|
|
934
921
|
function parseAgentOutput(stdout) {
|
|
935
|
-
const
|
|
936
|
-
return { resultSummary:
|
|
922
|
+
const { text, usage, sessionId } = shared.parseStreamJsonOutput(stdout, { maxTextLength: 2000 });
|
|
923
|
+
return { resultSummary: text, taskUsage: usage, sessionId };
|
|
937
924
|
}
|
|
938
925
|
|
|
939
926
|
/**
|
|
@@ -1018,7 +1005,16 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1018
1005
|
const meta = dispatchItem.meta;
|
|
1019
1006
|
const isSuccess = code === 0;
|
|
1020
1007
|
const result = isSuccess ? 'success' : 'error';
|
|
1021
|
-
const { resultSummary, taskUsage } = parseAgentOutput(stdout);
|
|
1008
|
+
const { resultSummary, taskUsage, sessionId } = parseAgentOutput(stdout);
|
|
1009
|
+
|
|
1010
|
+
// Save session for potential resume on next dispatch
|
|
1011
|
+
if (isSuccess && sessionId && agentId && !agentId.startsWith('temp-')) {
|
|
1012
|
+
try {
|
|
1013
|
+
shared.safeWrite(path.join(AGENTS_DIR, agentId, 'session.json'), {
|
|
1014
|
+
sessionId, dispatchId: dispatchItem.id, savedAt: new Date().toISOString()
|
|
1015
|
+
});
|
|
1016
|
+
} catch {}
|
|
1017
|
+
}
|
|
1022
1018
|
|
|
1023
1019
|
// Handle decomposition results — create sub-items from decompose agent output
|
|
1024
1020
|
let skipDoneStatus = false;
|
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) =>
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
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:
|
|
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
|
-
|
|
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
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. **
|
|
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
|