@yemi33/minions 0.1.206 → 0.1.207
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 +15 -0
- package/dashboard/js/refresh.js +42 -119
- package/dashboard/js/render-plans.js +1 -1
- package/dashboard/js/settings.js +10 -0
- package/dashboard.js +10 -1
- package/engine/lifecycle.js +35 -34
- package/engine/queries.js +41 -2
- package/engine/shared.js +29 -38
- package/engine/timeout.js +2 -0
- package/engine.js +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.207 (2026-04-02)
|
|
4
|
+
|
|
5
|
+
### Engine
|
|
6
|
+
- engine.js
|
|
7
|
+
- engine/lifecycle.js
|
|
8
|
+
- engine/queries.js
|
|
9
|
+
- engine/shared.js
|
|
10
|
+
- engine/timeout.js
|
|
11
|
+
|
|
12
|
+
### Dashboard
|
|
13
|
+
- dashboard.js
|
|
14
|
+
- dashboard/js/refresh.js
|
|
15
|
+
- dashboard/js/render-plans.js
|
|
16
|
+
- dashboard/js/settings.js
|
|
17
|
+
|
|
3
18
|
## 0.1.206 (2026-04-02)
|
|
4
19
|
|
|
5
20
|
### Other
|
package/dashboard/js/refresh.js
CHANGED
|
@@ -1,90 +1,30 @@
|
|
|
1
1
|
// refresh.js — Main refresh loop and initialization extracted from dashboard.html
|
|
2
2
|
|
|
3
3
|
// Sidebar activity indicators — detect changes between refreshes
|
|
4
|
+
// Registry: add one line per page. Counter returns a value; badge shows when value increases.
|
|
5
|
+
const _pageCounters = {
|
|
6
|
+
home: function(d) { return (d.dispatch?.completed || []).length; },
|
|
7
|
+
work: function(d) { return (d.workItems || []).length + '|' + (d.workItems || []).filter(function(w) { return w.status === 'done' || w.status === 'failed'; }).length; },
|
|
8
|
+
plans: function(d) { return (d.prdProgress?.complete || 0) + '|' + (d.plans || []).length; },
|
|
9
|
+
prs: function(d) { return (d.pullRequests || []).filter(function(p) { return p.status === 'merged'; }).length; },
|
|
10
|
+
inbox: function(d) { return (d.inbox?.items || []).length; },
|
|
11
|
+
meetings: function(d) { return (d.meetings || []).reduce(function(s, m) { return s + (m.round || 0); }, 0); },
|
|
12
|
+
pipelines: function(d) { return (d.pipelines || []).reduce(function(s, p) { return s + (p.runs || []).length; }, 0); },
|
|
13
|
+
schedule: function(d) { return (d.schedules || []).length; },
|
|
14
|
+
engine: function(d) { return (d.dispatch?.active || []).length; },
|
|
15
|
+
};
|
|
4
16
|
let _prevCounts = {};
|
|
5
|
-
let _prevEngineAlert = false;
|
|
6
17
|
function _detectPageChanges(data) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
workDispatched: (data.workItems || []).filter(w => w.status === 'dispatched').length,
|
|
13
|
-
prdComplete: data.prdProgress?.complete || 0,
|
|
14
|
-
prdInProgress: data.prdProgress?.inProgress || 0,
|
|
15
|
-
plansTotal: (data.plans || []).length,
|
|
16
|
-
prsTotal: (data.pullRequests || []).length,
|
|
17
|
-
prsMerged: (data.pullRequests || []).filter(p => p.status === 'merged').length,
|
|
18
|
-
prsReviewed: (data.pullRequests || []).filter(p => p.reviewStatus === 'approved' || p.reviewStatus === 'changes-requested').length,
|
|
19
|
-
inbox: (data.inbox?.items || []).length,
|
|
20
|
-
kbTotal: Object.values(data.knowledgeBase || {}).reduce((s, v) => s + (Array.isArray(v) ? v.length : 0), 0),
|
|
21
|
-
skillsTotal: (data.skills || []).length,
|
|
22
|
-
mcpTotal: (data.mcpServers || []).length,
|
|
23
|
-
scheduleTotal: (data.schedules || []).length,
|
|
24
|
-
pipelineRuns: (data.pipelines || []).reduce((s, p) => s + (p.runs || []).length, 0),
|
|
25
|
-
pipelineActive: (data.pipelines || []).filter(p => (p.runs || []).some(r => r.status === 'running')).length,
|
|
26
|
-
meetingRounds: (data.meetings || []).reduce((s, m) => s + (m.round || 0), 0),
|
|
27
|
-
meetingTotal: (data.meetings || []).length,
|
|
28
|
-
};
|
|
29
|
-
const changes = {};
|
|
30
|
-
if (_prevCounts.completions !== undefined) {
|
|
31
|
-
if (counts.completions > _prevCounts.completions || counts.activeDispatches > _prevCounts.activeDispatches) changes.home = true;
|
|
32
|
-
if (counts.workDone > _prevCounts.workDone || counts.workTotal > _prevCounts.workTotal || counts.workDispatched !== _prevCounts.workDispatched) changes.work = true;
|
|
33
|
-
if (counts.prdComplete > _prevCounts.prdComplete || counts.prdInProgress > _prevCounts.prdInProgress || counts.plansTotal > _prevCounts.plansTotal) changes.plans = true;
|
|
34
|
-
if (counts.prsTotal > _prevCounts.prsTotal || counts.prsMerged > _prevCounts.prsMerged || counts.prsReviewed > _prevCounts.prsReviewed) changes.prs = true;
|
|
35
|
-
if (counts.inbox > _prevCounts.inbox || counts.kbTotal > _prevCounts.kbTotal) changes.inbox = true;
|
|
36
|
-
if (counts.skillsTotal > _prevCounts.skillsTotal || counts.mcpTotal > _prevCounts.mcpTotal) changes.tools = true;
|
|
37
|
-
if (counts.scheduleTotal > _prevCounts.scheduleTotal) changes.schedule = true;
|
|
38
|
-
if (counts.pipelineRuns > _prevCounts.pipelineRuns || counts.pipelineActive > _prevCounts.pipelineActive) changes.pipelines = true;
|
|
39
|
-
if (counts.meetingRounds > _prevCounts.meetingRounds || counts.meetingTotal > _prevCounts.meetingTotal) changes.meetings = true;
|
|
18
|
+
var changes = {};
|
|
19
|
+
var counts = {};
|
|
20
|
+
for (var page in _pageCounters) {
|
|
21
|
+
counts[page] = String(_pageCounters[page](data));
|
|
22
|
+
if (_prevCounts[page] !== undefined && counts[page] !== _prevCounts[page]) changes[page] = true;
|
|
40
23
|
}
|
|
41
24
|
_prevCounts = counts;
|
|
42
|
-
|
|
43
|
-
// Engine page — only badge for genuine problems, not routine activity
|
|
44
|
-
const engineAlert = _isEngineAlertWorthy(data);
|
|
45
|
-
if (engineAlert && !_prevEngineAlert) changes.engine = true;
|
|
46
|
-
// Clear the engine badge when alert condition resolves
|
|
47
|
-
if (!engineAlert && _prevEngineAlert) {
|
|
48
|
-
const engineLink = document.querySelector('.sidebar-link[data-page="engine"]');
|
|
49
|
-
if (engineLink) clearNotifBadge(engineLink);
|
|
50
|
-
}
|
|
51
|
-
_prevEngineAlert = engineAlert;
|
|
52
|
-
|
|
53
25
|
return changes;
|
|
54
26
|
}
|
|
55
27
|
|
|
56
|
-
/**
|
|
57
|
-
* Determine if the engine state warrants a notification dot.
|
|
58
|
-
* Returns true only for genuine problems:
|
|
59
|
-
* - Engine stopped, stale, or in error state
|
|
60
|
-
* - 3+ failed work items in the last hour
|
|
61
|
-
* - Agent timeout/crash detected (error results in recent completions)
|
|
62
|
-
*/
|
|
63
|
-
function _isEngineAlertWorthy(data) {
|
|
64
|
-
// 1. Engine not running (stopped, stale, or error)
|
|
65
|
-
const engineState = data.engine?.state || 'stopped';
|
|
66
|
-
if (engineState === 'stopped' || engineState === 'error') return true;
|
|
67
|
-
// Stale heartbeat (>2 min old while claiming running)
|
|
68
|
-
if (engineState === 'running' && data.engine?.heartbeat) {
|
|
69
|
-
if (Date.now() - data.engine.heartbeat > 120000) return true;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// 2. 3+ failed work items in the last hour
|
|
73
|
-
const oneHourAgo = Date.now() - 3600000;
|
|
74
|
-
const recentFailures = (data.workItems || []).filter(w =>
|
|
75
|
-
w.status === 'failed' && w.updated_at && new Date(w.updated_at).getTime() > oneHourAgo
|
|
76
|
-
);
|
|
77
|
-
if (recentFailures.length >= 3) return true;
|
|
78
|
-
|
|
79
|
-
// 3. Agent timeout/crash — 3+ error results in recent completed dispatches
|
|
80
|
-
const recentErrors = (data.dispatch?.completed || []).filter(d =>
|
|
81
|
-
d.result === 'error' && d.completed_at && new Date(d.completed_at).getTime() > oneHourAgo
|
|
82
|
-
);
|
|
83
|
-
if (recentErrors.length >= 3) return true;
|
|
84
|
-
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
28
|
function _processStatusUpdate(data) {
|
|
89
29
|
// Detect fresh install — clear stale browser state if install ID changed
|
|
90
30
|
if (data.installId) {
|
|
@@ -97,24 +37,15 @@ function _processStatusUpdate(data) {
|
|
|
97
37
|
}
|
|
98
38
|
document.getElementById('ts').textContent = new Date(data.timestamp).toLocaleTimeString();
|
|
99
39
|
const engineState = (data.engine && data.engine.state) ? data.engine.state : 'stopped';
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const _r = (fn) => { try { fn(); } catch (e) { console.error('render error:', e.message); } };
|
|
110
|
-
_r(() => renderAgents(data.agents));
|
|
111
|
-
_r(() => renderPrdProgress(data.prdProgress));
|
|
112
|
-
_r(() => _cachePrdItems(data.prdProgress));
|
|
113
|
-
_r(() => renderInbox(data.inbox));
|
|
114
|
-
_r(() => cmdUpdateAgentList(data.agents));
|
|
115
|
-
_r(() => cmdUpdateProjectList(data.projects || []));
|
|
116
|
-
_r(() => renderNotes(data.notes));
|
|
117
|
-
_r(() => renderPrd(data.prd, data.prdProgress));
|
|
40
|
+
document.getElementById('setup-banner').style.display = (!data.initialized && engineState !== 'stopped') ? 'block' : 'none';
|
|
41
|
+
renderAgents(data.agents);
|
|
42
|
+
renderPrdProgress(data.prdProgress);
|
|
43
|
+
_cachePrdItems(data.prdProgress);
|
|
44
|
+
renderInbox(data.inbox);
|
|
45
|
+
cmdUpdateAgentList(data.agents);
|
|
46
|
+
cmdUpdateProjectList(data.projects || []);
|
|
47
|
+
renderNotes(data.notes);
|
|
48
|
+
renderPrd(data.prd, data.prdProgress);
|
|
118
49
|
// Auto-approve badge
|
|
119
50
|
const autoEl = document.getElementById('auto-approve-badge');
|
|
120
51
|
if (autoEl) autoEl.innerHTML = data.autoMode?.approvePlans
|
|
@@ -123,23 +54,24 @@ function _processStatusUpdate(data) {
|
|
|
123
54
|
// Inbox consolidation threshold from config
|
|
124
55
|
const threshEl = document.getElementById('inbox-threshold');
|
|
125
56
|
if (threshEl && data.autoMode?.inboxThreshold) threshEl.textContent = data.autoMode.inboxThreshold;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
57
|
+
renderPrs(data.pullRequests || []);
|
|
58
|
+
renderArchiveButtons(data.archivedPrds || []);
|
|
59
|
+
renderEngineStatus(data.engine);
|
|
60
|
+
renderDispatch(data.dispatch);
|
|
129
61
|
window._lastDispatch = data.dispatch;
|
|
130
62
|
window._lastWorkItems = data.workItems || [];
|
|
131
63
|
window._lastStatus = data;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
64
|
+
prunePrdRequeueState(window._lastWorkItems);
|
|
65
|
+
renderEngineLog(data.engineLog || []);
|
|
66
|
+
renderProjects(data.projects || []);
|
|
67
|
+
renderMetrics(data.metrics || {});
|
|
68
|
+
renderWorkItems(data.workItems || []);
|
|
69
|
+
renderSkills(data.skills || []);
|
|
70
|
+
renderMcpServers(data.mcpServers || []);
|
|
71
|
+
renderSchedules(data.schedules || []);
|
|
72
|
+
renderMeetings(data.meetings || []);
|
|
73
|
+
if (typeof renderPipelines === 'function') renderPipelines(data.pipelines || []);
|
|
74
|
+
renderPinned(data.pinned || []);
|
|
143
75
|
// Update sidebar counts
|
|
144
76
|
const swi = document.getElementById('sidebar-wi');
|
|
145
77
|
if (swi) swi.textContent = (data.workItems || []).length || '';
|
|
@@ -170,16 +102,7 @@ async function refresh() {
|
|
|
170
102
|
refresh();
|
|
171
103
|
|
|
172
104
|
// Poll for status updates (SSE caused HTTP/1.1 connection exhaustion — CC fetch would fail)
|
|
173
|
-
|
|
174
|
-
document.addEventListener('visibilitychange', function() {
|
|
175
|
-
if (document.hidden) {
|
|
176
|
-
clearInterval(_refreshTimer);
|
|
177
|
-
_refreshTimer = null;
|
|
178
|
-
} else if (!_refreshTimer) {
|
|
179
|
-
refresh();
|
|
180
|
-
_refreshTimer = setInterval(refresh, 4000);
|
|
181
|
-
}
|
|
182
|
-
});
|
|
105
|
+
setInterval(refresh, 4000);
|
|
183
106
|
|
|
184
107
|
// Wire sidebar navigation
|
|
185
108
|
document.querySelectorAll('.sidebar-link').forEach(link => {
|
|
@@ -223,7 +223,7 @@ function renderPlans(plans) {
|
|
|
223
223
|
actions = '<div class="plan-card-meta" style="margin-top:6px;color:var(--purple,#a855f7)">Revision in progress: ' + escHtml((p.revisionFeedback || '').slice(0, 100)) + '</div>';
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
const executeBtn = isDraft && effectiveStatus === 'active' && !isArchived && !prdFile ? '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--green);font-weight:600" ' +
|
|
226
|
+
const executeBtn = isDraft && (effectiveStatus === 'active' || effectiveStatus === 'draft') && !isArchived && !prdFile ? '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--green);font-weight:600" ' +
|
|
227
227
|
'onclick="event.stopPropagation();planExecute(\'' + escHtml(p.file) + '\',\'' + escHtml(p.project) + '\',this)">Execute</button>' : '';
|
|
228
228
|
const showPause = effectiveStatus === 'in-progress' && prdFile && !isArchived;
|
|
229
229
|
const showResume = (effectiveStatus === 'paused' || effectiveStatus === 'awaiting-approval') && prdFile && !isArchived;
|
package/dashboard/js/settings.js
CHANGED
|
@@ -54,6 +54,15 @@ async function openSettings() {
|
|
|
54
54
|
settingsField('Output Format', 'set-outputFormat', c.outputFormat || 'stream-json', '', '') +
|
|
55
55
|
settingsField('Allowed Tools', 'set-allowedTools', c.allowedTools || '', '', 'Comma-separated (empty = all)') +
|
|
56
56
|
'</div>' +
|
|
57
|
+
'<div style="margin-bottom:16px">' +
|
|
58
|
+
'<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">Permission Mode <span style="opacity:0.6">(how agents handle tool approvals)</span></label>' +
|
|
59
|
+
'<select id="set-permissionMode" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:12px">' +
|
|
60
|
+
'<option value="bypassPermissions"' + ((c.permissionMode || 'bypassPermissions') === 'bypassPermissions' ? ' selected' : '') + '>Bypass (recommended) — agents run without permission prompts</option>' +
|
|
61
|
+
'<option value="auto"' + ((c.permissionMode) === 'auto' ? ' selected' : '') + '>Auto — agents auto-approve safe tools, prompt for risky ones</option>' +
|
|
62
|
+
'<option value="default"' + ((c.permissionMode) === 'default' ? ' selected' : '') + '>Default — agents prompt for every tool (will hang without a human)</option>' +
|
|
63
|
+
'</select>' +
|
|
64
|
+
'<div style="font-size:9px;color:var(--muted);margin-top:2px">Non-bypass modes require a human watching the agent terminal to approve tool calls</div>' +
|
|
65
|
+
'</div>' +
|
|
57
66
|
|
|
58
67
|
'<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Agents</h3>' +
|
|
59
68
|
'<table style="width:100%;border-collapse:collapse;margin-bottom:16px;font-size:11px">' +
|
|
@@ -138,6 +147,7 @@ async function saveSettings() {
|
|
|
138
147
|
const claudePayload = {
|
|
139
148
|
outputFormat: document.getElementById('set-outputFormat').value,
|
|
140
149
|
allowedTools: document.getElementById('set-allowedTools').value,
|
|
150
|
+
permissionMode: document.getElementById('set-permissionMode').value,
|
|
141
151
|
};
|
|
142
152
|
|
|
143
153
|
const agentsPayload = {};
|
package/dashboard.js
CHANGED
|
@@ -2711,7 +2711,12 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2711
2711
|
const { execSync: ex } = require('child_process');
|
|
2712
2712
|
const detected = { name: path.basename(target), _found: [] };
|
|
2713
2713
|
try {
|
|
2714
|
-
|
|
2714
|
+
let head = '';
|
|
2715
|
+
try {
|
|
2716
|
+
head = ex('git symbolic-ref refs/remotes/origin/HEAD', { cwd: target, encoding: 'utf8', timeout: 5000 }).trim();
|
|
2717
|
+
} catch {
|
|
2718
|
+
head = ex('git symbolic-ref HEAD', { cwd: target, encoding: 'utf8', timeout: 5000 }).trim();
|
|
2719
|
+
}
|
|
2715
2720
|
detected.mainBranch = head.replace('refs/remotes/origin/', '').replace('refs/heads/', '');
|
|
2716
2721
|
} catch { detected.mainBranch = 'main'; }
|
|
2717
2722
|
try {
|
|
@@ -2982,6 +2987,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2982
2987
|
for (const key of ['allowedTools', 'outputFormat']) {
|
|
2983
2988
|
if (body.claude[key] !== undefined) config.claude[key] = String(body.claude[key]);
|
|
2984
2989
|
}
|
|
2990
|
+
if (body.claude.permissionMode !== undefined) {
|
|
2991
|
+
const valid = ['bypassPermissions', 'auto', 'default'];
|
|
2992
|
+
config.claude.permissionMode = valid.includes(body.claude.permissionMode) ? body.claude.permissionMode : 'bypassPermissions';
|
|
2993
|
+
}
|
|
2985
2994
|
}
|
|
2986
2995
|
|
|
2987
2996
|
if (body.agents) {
|
package/engine/lifecycle.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const shared = require('./shared');
|
|
9
|
-
const { safeRead, safeJson, safeWrite, safeReadDir, execSilent, projectPrPath, getPrLinks,
|
|
9
|
+
const { safeRead, safeJson, safeWrite, safeReadDir, execSilent, projectPrPath, getPrLinks, addPrLink,
|
|
10
10
|
mutateJsonFileLocked, log, ts, dateStamp } = shared;
|
|
11
11
|
const { trackEngineUsage } = require('./llm');
|
|
12
12
|
const queries = require('./queries');
|
|
@@ -58,7 +58,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
58
58
|
const unmaterialized = [...planFeatureIds].filter(id => {
|
|
59
59
|
if (workItemById[id]) return false;
|
|
60
60
|
const prdItem = (plan.missing_features || []).find(f => f.id === id);
|
|
61
|
-
return !(prdItem && (prdItem.status === 'done'));
|
|
61
|
+
return !(prdItem && (prdItem.status === 'done' || prdItem.status === 'in-pr'));
|
|
62
62
|
});
|
|
63
63
|
if (unmaterialized.length > 0) {
|
|
64
64
|
log('info', `Plan ${planFile}: ${unmaterialized.length}/${planFeatureIds.size} feature(s) not yet materialized as work items: ${unmaterialized.join(', ')}`);
|
|
@@ -68,16 +68,16 @@ function checkPlanCompletion(meta, config) {
|
|
|
68
68
|
// Check 2: every feature's work item must be done (or PRD item marked done externally)
|
|
69
69
|
const notDone = [...planFeatureIds].filter(id => {
|
|
70
70
|
const w = workItemById[id];
|
|
71
|
-
if (w && (w.status === 'done')) return false; // in-pr accepted for backward compat
|
|
71
|
+
if (w && (w.status === 'done' || w.status === 'in-pr')) return false; // in-pr accepted for backward compat
|
|
72
72
|
const prdItem = (plan.missing_features || []).find(f => f.id === id);
|
|
73
|
-
return !(prdItem && (prdItem.status === 'done'));
|
|
73
|
+
return !(prdItem && (prdItem.status === 'done' || prdItem.status === 'in-pr'));
|
|
74
74
|
});
|
|
75
75
|
if (notDone.length > 0) {
|
|
76
76
|
log('info', `Plan ${planFile}: waiting for done on ${notDone.length}/${planFeatureIds.size} item(s): ${notDone.join(', ')}`);
|
|
77
77
|
return;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
const doneItems = planItems.filter(w => w.status === 'done');
|
|
80
|
+
const doneItems = planItems.filter(w => w.status === 'done' || w.status === 'in-pr');
|
|
81
81
|
const failedItems = planItems.filter(w => w.status === 'failed');
|
|
82
82
|
|
|
83
83
|
// 1. Mark plan as completed
|
|
@@ -475,6 +475,7 @@ function updateWorkItemStatus(meta, status, reason) {
|
|
|
475
475
|
}
|
|
476
476
|
} else {
|
|
477
477
|
target.status = status;
|
|
478
|
+
delete target._pendingReason;
|
|
478
479
|
if (status === 'done') {
|
|
479
480
|
delete target.failReason;
|
|
480
481
|
delete target.failedAt;
|
|
@@ -534,7 +535,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
534
535
|
for (const block of content) {
|
|
535
536
|
if (block.type === 'tool_result' && block.content) {
|
|
536
537
|
const text = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
|
|
537
|
-
if (text.includes('pullRequestId') || text.includes('create_pull_request')) {
|
|
538
|
+
if (text.includes('pullRequestId') || text.includes('create_pull_request') || text.includes('/pull/') || text.includes('pullrequest/')) {
|
|
538
539
|
while ((match = urlPattern.exec(text)) !== null) prMatches.add(match[1] || match[2]);
|
|
539
540
|
}
|
|
540
541
|
}
|
|
@@ -624,10 +625,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
624
625
|
sourcePlan: meta?.item?.sourcePlan || '',
|
|
625
626
|
itemType: meta?.item?.itemType || ''
|
|
626
627
|
});
|
|
627
|
-
if (meta?.item?.id)
|
|
628
|
-
const project = config ? shared.getProjects(config)[0] : null;
|
|
629
|
-
if (project) shared.linkPrToItem(project, fullId, meta.item.id);
|
|
630
|
-
}
|
|
628
|
+
if (meta?.item?.id) addPrLink(fullId, meta.item.id);
|
|
631
629
|
added++;
|
|
632
630
|
}
|
|
633
631
|
|
|
@@ -735,7 +733,7 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
735
733
|
// Mark review as approved since it was merged
|
|
736
734
|
pr.reviewStatus = 'approved';
|
|
737
735
|
|
|
738
|
-
// Resolve linked work item from
|
|
736
|
+
// Resolve linked work item from pr-links or PR branch name
|
|
739
737
|
let mergedItemId = getPrLinks()[pr.id];
|
|
740
738
|
if (!mergedItemId && pr.branch) {
|
|
741
739
|
const branchMatch = pr.branch.match(/(P-[a-z0-9]{6,})/i) || pr.branch.match(/(W-[a-z0-9]+)/i);
|
|
@@ -752,13 +750,13 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
752
750
|
const plan = safeJson(path.join(prdDir, pf));
|
|
753
751
|
if (!plan?.missing_features) continue;
|
|
754
752
|
const feature = plan.missing_features.find(f => f.id === mergedItemId);
|
|
755
|
-
if (feature && feature.status !== '
|
|
756
|
-
feature.status = '
|
|
753
|
+
if (feature && feature.status !== 'implemented') {
|
|
754
|
+
feature.status = 'implemented';
|
|
757
755
|
shared.safeWrite(path.join(prdDir, pf), plan);
|
|
758
756
|
updated++;
|
|
759
757
|
}
|
|
760
758
|
}
|
|
761
|
-
if (updated > 0) log('info', `Post-merge: marked ${mergedItemId} as
|
|
759
|
+
if (updated > 0) log('info', `Post-merge: marked ${mergedItemId} as implemented for ${pr.id}`);
|
|
762
760
|
} catch (err) { log('warn', `Post-merge PRD update: ${err.message}`); }
|
|
763
761
|
|
|
764
762
|
// Mark work item as done
|
|
@@ -1224,40 +1222,42 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1224
1222
|
|
|
1225
1223
|
if (isSuccess && meta?.item?.id && !skipDoneStatus) updateWorkItemStatus(meta, 'done', '');
|
|
1226
1224
|
|
|
1227
|
-
// Auto-dispatch
|
|
1228
|
-
if (isSuccess && !skipDoneStatus && type === 'implement' && meta?.item?.id) {
|
|
1229
|
-
const
|
|
1230
|
-
if (
|
|
1225
|
+
// Auto-dispatch evaluate work item after implement or fix completes successfully
|
|
1226
|
+
if (isSuccess && !skipDoneStatus && (type === 'implement' || (type === 'fix' && meta?.item?._evalParentId)) && meta?.item?.id) {
|
|
1227
|
+
const evalLoop = config.engine?.evalLoop ?? shared.ENGINE_DEFAULTS.evalLoop;
|
|
1228
|
+
if (evalLoop) {
|
|
1231
1229
|
try {
|
|
1232
1230
|
const wiPath = resolveWiPath(meta);
|
|
1233
1231
|
if (wiPath) {
|
|
1234
1232
|
const items = safeJson(wiPath) || [];
|
|
1235
|
-
//
|
|
1236
|
-
const
|
|
1233
|
+
// For fix items, target the original implement parent; for implement, target self
|
|
1234
|
+
const evalTargetId = type === 'fix' ? meta.item._evalParentId : meta.item.id;
|
|
1235
|
+
// Dedup: skip if an evaluate item already exists for this parent
|
|
1236
|
+
const existing = items.find(i => i._evalParentId === evalTargetId && i.type === 'evaluate' && i.status === 'pending');
|
|
1237
1237
|
if (existing) {
|
|
1238
|
-
log('info', `Eval loop:
|
|
1238
|
+
log('info', `Eval loop: evaluate item ${existing.id} already exists for ${evalTargetId}, skipping`);
|
|
1239
1239
|
} else {
|
|
1240
|
-
const parentItem = items.find(i => i.id ===
|
|
1240
|
+
const parentItem = items.find(i => i.id === evalTargetId);
|
|
1241
1241
|
const evalItem = {
|
|
1242
1242
|
id: 'W-' + shared.uid(),
|
|
1243
|
-
title: `
|
|
1244
|
-
type: '
|
|
1243
|
+
title: `Evaluate: ${parentItem?.title || meta.item.title || evalTargetId}`,
|
|
1244
|
+
type: 'evaluate',
|
|
1245
1245
|
priority: meta.item.priority || 'high',
|
|
1246
1246
|
status: 'pending',
|
|
1247
1247
|
created: ts(),
|
|
1248
1248
|
createdBy: 'engine:eval-loop',
|
|
1249
1249
|
project: meta.project?.name || meta.item.project,
|
|
1250
|
-
branch_name: parentItem?.branch_name || meta.branch || null,
|
|
1251
|
-
pr_url: parentItem?.pr_url || null,
|
|
1250
|
+
branch_name: parentItem?.branch_name || meta.item.branch_name || meta.branch || null,
|
|
1251
|
+
pr_url: parentItem?.pr_url || meta.item.pr_url || null,
|
|
1252
1252
|
acceptance_criteria: parentItem?.acceptance_criteria || meta.item.acceptance_criteria || null,
|
|
1253
|
-
_evalParentId:
|
|
1253
|
+
_evalParentId: evalTargetId,
|
|
1254
1254
|
};
|
|
1255
1255
|
if (parentItem?.sourcePlan) evalItem.sourcePlan = parentItem.sourcePlan;
|
|
1256
1256
|
// Mark parent as eval-dispatched before writing to prevent duplicates on re-run
|
|
1257
1257
|
if (parentItem) parentItem._evalDispatched = true;
|
|
1258
1258
|
items.push(evalItem);
|
|
1259
1259
|
shared.safeWrite(wiPath, items);
|
|
1260
|
-
log('info', `Eval loop: created ${evalItem.id} for completed
|
|
1260
|
+
log('info', `Eval loop: created ${evalItem.id} for completed ${type} ${meta.item.id} (parent: ${evalTargetId})`);
|
|
1261
1261
|
}
|
|
1262
1262
|
}
|
|
1263
1263
|
} catch (err) {
|
|
@@ -1266,14 +1266,14 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1266
1266
|
}
|
|
1267
1267
|
}
|
|
1268
1268
|
|
|
1269
|
-
//
|
|
1270
|
-
if (isSuccess && type === '
|
|
1269
|
+
// Evaluate completion: parse verdict and handle eval→fix iteration loop
|
|
1270
|
+
if (isSuccess && type === 'evaluate' && meta?.item?._evalParentId) {
|
|
1271
1271
|
try {
|
|
1272
1272
|
const verdict = parseEvalVerdict(resultSummary || stdout);
|
|
1273
|
-
const
|
|
1273
|
+
const evalLoop = config.engine?.evalLoop ?? shared.ENGINE_DEFAULTS.evalLoop;
|
|
1274
1274
|
const maxIter = config.engine?.evalMaxIterations ?? shared.ENGINE_DEFAULTS.evalMaxIterations;
|
|
1275
1275
|
|
|
1276
|
-
if (verdict && !verdict.pass &&
|
|
1276
|
+
if (verdict && !verdict.pass && evalLoop) {
|
|
1277
1277
|
const wiPath = resolveWiPath(meta);
|
|
1278
1278
|
if (wiPath) {
|
|
1279
1279
|
const items = safeJson(wiPath) || [];
|
|
@@ -1420,8 +1420,9 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1420
1420
|
}
|
|
1421
1421
|
}
|
|
1422
1422
|
|
|
1423
|
-
// Detect implement tasks that completed without creating a PR
|
|
1424
|
-
|
|
1423
|
+
// Detect implement tasks that completed without creating a PR.
|
|
1424
|
+
// Exempt: tasks operating on an existing PR (meta.pr set) or human feedback fix tasks.
|
|
1425
|
+
if (isSuccess && (type === 'implement' || type === 'implement:large' || type === 'fix') && prsCreatedCount === 0 && meta?.item?.id && !meta.pr && meta.source !== 'pr-human-feedback') {
|
|
1425
1426
|
// Check if a PR already exists linked to this work item (from a previous attempt)
|
|
1426
1427
|
const projects = shared.getProjects(config);
|
|
1427
1428
|
const existingPrFound = Object.values(getPrLinks()).includes(meta.item.id);
|
package/engine/queries.js
CHANGED
|
@@ -339,15 +339,21 @@ function collectSkillFiles(config) {
|
|
|
339
339
|
try { return fs.statSync(path.join(claudeSkillsDir, d)).isDirectory(); } catch { return false; }
|
|
340
340
|
});
|
|
341
341
|
for (const d of dirs) {
|
|
342
|
+
// Check both <name>/SKILL.md and <name>/skills/SKILL.md (Claude Code uses both)
|
|
342
343
|
const skillFile = path.join(claudeSkillsDir, d, 'SKILL.md');
|
|
344
|
+
const nestedSkillFile = path.join(claudeSkillsDir, d, 'skills', 'SKILL.md');
|
|
343
345
|
if (fs.existsSync(skillFile)) {
|
|
344
346
|
skillFiles.push({ file: 'SKILL.md', dir: path.join(claudeSkillsDir, d), scope: 'claude-code', skillName: d });
|
|
345
347
|
seen.add(d);
|
|
348
|
+
} else if (fs.existsSync(nestedSkillFile)) {
|
|
349
|
+
skillFiles.push({ file: 'SKILL.md', dir: path.join(claudeSkillsDir, d, 'skills'), scope: 'claude-code', skillName: d });
|
|
350
|
+
seen.add(d);
|
|
346
351
|
}
|
|
347
352
|
}
|
|
348
353
|
} catch { /* optional */ }
|
|
349
354
|
|
|
350
|
-
// 1b. Installed plugin skills: ~/.claude/plugins/installed_plugins.json
|
|
355
|
+
// 1b. Installed plugin skills: ~/.claude/plugins/installed_plugins.json
|
|
356
|
+
// Plugins use commands/*.md and/or skills/<name>/SKILL.md and/or skills/SKILL.md
|
|
351
357
|
try {
|
|
352
358
|
const pluginsFile = path.join(homeDir, '.claude', 'plugins', 'installed_plugins.json');
|
|
353
359
|
const registry = JSON.parse(safeRead(pluginsFile) || '{}');
|
|
@@ -355,16 +361,49 @@ function collectSkillFiles(config) {
|
|
|
355
361
|
if (!Array.isArray(installs) || installs.length === 0) continue;
|
|
356
362
|
const install = installs[0];
|
|
357
363
|
if (!install.installPath) continue;
|
|
364
|
+
const pluginName = pluginKey.split('@')[0];
|
|
365
|
+
|
|
366
|
+
// commands/*.md (older style)
|
|
358
367
|
const commandsDir = path.join(install.installPath, 'commands');
|
|
359
368
|
try {
|
|
360
369
|
const commands = fs.readdirSync(commandsDir).filter(f => f.endsWith('.md'));
|
|
361
370
|
for (const cmd of commands) {
|
|
362
|
-
const name =
|
|
371
|
+
const name = pluginName + ':' + cmd.replace('.md', '');
|
|
363
372
|
if (seen.has(name)) continue;
|
|
364
373
|
skillFiles.push({ file: cmd, dir: commandsDir, scope: 'plugin', skillName: name });
|
|
365
374
|
seen.add(name);
|
|
366
375
|
}
|
|
367
376
|
} catch { /* optional */ }
|
|
377
|
+
|
|
378
|
+
// skills/<name>/SKILL.md or skills/SKILL.md (newer style)
|
|
379
|
+
const skillsDir = path.join(install.installPath, 'skills');
|
|
380
|
+
try {
|
|
381
|
+
const entries = fs.readdirSync(skillsDir);
|
|
382
|
+
for (const entry of entries) {
|
|
383
|
+
const entryPath = path.join(skillsDir, entry);
|
|
384
|
+
if (entry === 'SKILL.md') {
|
|
385
|
+
// Flat: skills/SKILL.md
|
|
386
|
+
const name = pluginName;
|
|
387
|
+
if (!seen.has(name)) {
|
|
388
|
+
skillFiles.push({ file: 'SKILL.md', dir: skillsDir, scope: 'plugin', skillName: name });
|
|
389
|
+
seen.add(name);
|
|
390
|
+
}
|
|
391
|
+
} else {
|
|
392
|
+
try {
|
|
393
|
+
if (!fs.statSync(entryPath).isDirectory()) continue;
|
|
394
|
+
} catch { continue; }
|
|
395
|
+
// Nested: skills/<name>/SKILL.md
|
|
396
|
+
const nestedSkill = path.join(entryPath, 'SKILL.md');
|
|
397
|
+
if (fs.existsSync(nestedSkill)) {
|
|
398
|
+
const name = pluginName + ':' + entry;
|
|
399
|
+
if (!seen.has(name)) {
|
|
400
|
+
skillFiles.push({ file: 'SKILL.md', dir: entryPath, scope: 'plugin', skillName: name });
|
|
401
|
+
seen.add(name);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
} catch { /* optional */ }
|
|
368
407
|
}
|
|
369
408
|
} catch { /* optional */ }
|
|
370
409
|
|
package/engine/shared.js
CHANGED
|
@@ -7,7 +7,7 @@ const fs = require('fs');
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
|
|
9
9
|
const MINIONS_DIR = path.resolve(__dirname, '..');
|
|
10
|
-
const
|
|
10
|
+
const PR_LINKS_PATH = path.join(MINIONS_DIR, 'engine', 'pr-links.json');
|
|
11
11
|
const LOG_PATH = path.join(__dirname, 'log.json');
|
|
12
12
|
|
|
13
13
|
// ── Timestamps & Logging ────────────────────────────────────────────────────
|
|
@@ -258,7 +258,6 @@ function gitEnv() {
|
|
|
258
258
|
* Single source of truth — used by llm.js, consolidation.js, and lifecycle.js.
|
|
259
259
|
*/
|
|
260
260
|
function parseStreamJsonOutput(raw, { maxTextLength = 0 } = {}) {
|
|
261
|
-
if (typeof raw !== 'string') raw = '';
|
|
262
261
|
let text = '';
|
|
263
262
|
let usage = null;
|
|
264
263
|
let sessionId = null;
|
|
@@ -501,52 +500,45 @@ function parseSkillFrontmatter(content, filename) {
|
|
|
501
500
|
// Never touched by polling loops — only written when a PR is first linked to a PRD item.
|
|
502
501
|
|
|
503
502
|
function getPrLinks() {
|
|
504
|
-
// Derive from PR.prdItems (single source of truth)
|
|
505
503
|
const links = {};
|
|
504
|
+
// Primary source: derive from all projects/*/pull-requests.json prdItems
|
|
505
|
+
const projectsDir = path.join(MINIONS_DIR, 'projects');
|
|
506
506
|
try {
|
|
507
|
-
const
|
|
508
|
-
for (const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
507
|
+
const fs = require('fs');
|
|
508
|
+
for (const d of fs.readdirSync(projectsDir, { withFileTypes: true })) {
|
|
509
|
+
if (!d.isDirectory()) continue;
|
|
510
|
+
try {
|
|
511
|
+
const prs = JSON.parse(fs.readFileSync(path.join(projectsDir, d.name, 'pull-requests.json'), 'utf8'));
|
|
512
|
+
for (const pr of prs) {
|
|
513
|
+
if (!pr.id) continue;
|
|
514
|
+
for (const itemId of (pr.prdItems || [])) {
|
|
515
|
+
if (itemId) links[pr.id] = itemId;
|
|
516
|
+
}
|
|
513
517
|
}
|
|
514
|
-
}
|
|
518
|
+
} catch { /* missing or invalid */ }
|
|
519
|
+
}
|
|
520
|
+
} catch { /* projects dir missing */ }
|
|
521
|
+
// Fallback: static pr-links.json for entries not covered above
|
|
522
|
+
try {
|
|
523
|
+
const static_ = JSON.parse(require('fs').readFileSync(PR_LINKS_PATH, 'utf8'));
|
|
524
|
+
for (const [k, v] of Object.entries(static_)) {
|
|
525
|
+
if (!links[k]) links[k] = v;
|
|
515
526
|
}
|
|
516
|
-
} catch { /*
|
|
527
|
+
} catch { /* missing */ }
|
|
517
528
|
return links;
|
|
518
529
|
}
|
|
519
530
|
|
|
520
|
-
|
|
521
|
-
* Locked mutation of a project's pull-requests.json.
|
|
522
|
-
* Single source of truth for PR data including prdItems links.
|
|
523
|
-
*/
|
|
524
|
-
function mutatePrs(project, mutateFn) {
|
|
525
|
-
const prPath = projectPrPath(project);
|
|
526
|
-
return mutateJsonFileLocked(prPath, (prs) => {
|
|
527
|
-
return mutateFn(Array.isArray(prs) ? prs : []);
|
|
528
|
-
}, { defaultValue: [] });
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
/**
|
|
532
|
-
* Link a PR to a work item via PR.prdItems (single source of truth).
|
|
533
|
-
* Uses file-locked mutation to prevent race conditions.
|
|
534
|
-
*/
|
|
535
|
-
function linkPrToItem(project, prId, itemId) {
|
|
531
|
+
function addPrLink(prId, itemId) {
|
|
536
532
|
if (!prId || !itemId) return;
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
if (!pr.prdItems.includes(itemId)) pr.prdItems.push(itemId);
|
|
542
|
-
}
|
|
543
|
-
return prs;
|
|
544
|
-
});
|
|
533
|
+
const links = getPrLinks();
|
|
534
|
+
if (links[prId] === itemId) return; // already correct, no write needed
|
|
535
|
+
links[prId] = itemId;
|
|
536
|
+
safeWrite(PR_LINKS_PATH, links);
|
|
545
537
|
}
|
|
546
538
|
|
|
547
539
|
module.exports = {
|
|
548
540
|
MINIONS_DIR,
|
|
549
|
-
|
|
541
|
+
PR_LINKS_PATH,
|
|
550
542
|
LOG_PATH,
|
|
551
543
|
ts,
|
|
552
544
|
logTs,
|
|
@@ -580,8 +572,7 @@ module.exports = {
|
|
|
580
572
|
projectWorkItemsPath,
|
|
581
573
|
projectPrPath,
|
|
582
574
|
getPrLinks,
|
|
583
|
-
|
|
584
|
-
linkPrToItem,
|
|
575
|
+
addPrLink,
|
|
585
576
|
nextWorkItemId,
|
|
586
577
|
getAdoOrgBase,
|
|
587
578
|
sanitizePath,
|
package/engine/timeout.js
CHANGED
|
@@ -256,11 +256,13 @@ function checkTimeouts(config) {
|
|
|
256
256
|
item._retryCount = retries + 1;
|
|
257
257
|
delete item.dispatched_at;
|
|
258
258
|
delete item.dispatched_to;
|
|
259
|
+
delete item._pendingReason;
|
|
259
260
|
} else {
|
|
260
261
|
log('warn', `Reconcile: work item ${item.id} failed after ${retries} retries — marking as failed`);
|
|
261
262
|
item.status = 'failed';
|
|
262
263
|
item.failReason = 'Agent died or was killed (3 retries exhausted)';
|
|
263
264
|
item.failedAt = ts();
|
|
265
|
+
delete item._pendingReason;
|
|
264
266
|
}
|
|
265
267
|
changed = true;
|
|
266
268
|
}
|
package/engine.js
CHANGED
|
@@ -396,6 +396,7 @@ function spawnAgent(dispatchItem, config) {
|
|
|
396
396
|
} else {
|
|
397
397
|
log('error', `Failed to create worktree for ${branchName}: ${err.message}${err.stderr ? '\n' + err.stderr.toString().slice(0, 500) : ''}`);
|
|
398
398
|
completeDispatch(id, 'error', 'Worktree creation failed: ' + (err.message || '').slice(0, 200));
|
|
399
|
+
try { updateMetrics(agentId, dispatchItem, 'error', null, 0, null); } catch { /* optional */ }
|
|
399
400
|
return null;
|
|
400
401
|
}
|
|
401
402
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.207",
|
|
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"
|