@yemi33/minions 0.1.1567 → 0.1.1569
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 +10 -0
- package/dashboard/js/render-other.js +35 -2
- package/dashboard/js/settings.js +13 -0
- package/dashboard.js +18 -4
- package/engine/lifecycle.js +30 -1
- package/engine/shared.js +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -21,6 +21,28 @@ function renderProjects(projects) {
|
|
|
21
21
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function _projectCachePath(project) {
|
|
25
|
+
return String((project && (project.localPath || project.path)) || '').replace(/\\/g, '/');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function optimisticallyAddProject(project) {
|
|
29
|
+
if (!project || !project.name) return;
|
|
30
|
+
const next = {
|
|
31
|
+
name: project.name,
|
|
32
|
+
description: project.description || '',
|
|
33
|
+
localPath: _projectCachePath(project),
|
|
34
|
+
path: _projectCachePath(project),
|
|
35
|
+
};
|
|
36
|
+
if (!window._lastStatus) window._lastStatus = {};
|
|
37
|
+
const current = Array.isArray(window._lastStatus.projects) ? window._lastStatus.projects.slice() : [];
|
|
38
|
+
const nextPath = _projectCachePath(next);
|
|
39
|
+
if (current.some(function(p) { return p.name === next.name || (nextPath && _projectCachePath(p) === nextPath); })) return;
|
|
40
|
+
current.push(next);
|
|
41
|
+
window._lastStatus.projects = current;
|
|
42
|
+
if (typeof cmdUpdateProjectList === 'function') cmdUpdateProjectList(current);
|
|
43
|
+
renderProjects(current);
|
|
44
|
+
}
|
|
45
|
+
|
|
24
46
|
async function projectChipRemove(name) {
|
|
25
47
|
if (!confirm('Remove project "' + name + '"? Pending work cancels, active agents are killed, data dir is archived to projects/.archived/.')) return;
|
|
26
48
|
showToast('cmd-toast', 'Removing project "' + name + '"...', true);
|
|
@@ -385,7 +407,18 @@ async function _addSelectedProjects() {
|
|
|
385
407
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
386
408
|
body: JSON.stringify({ path: repo.path })
|
|
387
409
|
});
|
|
388
|
-
|
|
410
|
+
var data = await res.json().catch(function() { return {}; });
|
|
411
|
+
if (res.ok) {
|
|
412
|
+
added++;
|
|
413
|
+
optimisticallyAddProject({
|
|
414
|
+
name: data.name || repo.name,
|
|
415
|
+
description: (data.detected && data.detected.description) || repo.description || '',
|
|
416
|
+
path: data.path || repo.path,
|
|
417
|
+
localPath: data.path || repo.path,
|
|
418
|
+
});
|
|
419
|
+
cb.disabled = true;
|
|
420
|
+
cb.closest('label').style.opacity = '0.5';
|
|
421
|
+
}
|
|
389
422
|
} catch { /* continue with next */ }
|
|
390
423
|
}
|
|
391
424
|
if (added > 0) {
|
|
@@ -394,4 +427,4 @@ async function _addSelectedProjects() {
|
|
|
394
427
|
}
|
|
395
428
|
}
|
|
396
429
|
|
|
397
|
-
window.MinionsOther = { renderProjects, projectChipRemove, renderMcpServers, renderMetrics, renderLlmPerf, renderTokenUsage, openScanProjectsModal };
|
|
430
|
+
window.MinionsOther = { renderProjects, optimisticallyAddProject, projectChipRemove, renderMcpServers, renderMetrics, renderLlmPerf, renderTokenUsage, openScanProjectsModal };
|
package/dashboard/js/settings.js
CHANGED
|
@@ -384,6 +384,19 @@ async function addProject() {
|
|
|
384
384
|
});
|
|
385
385
|
const addData = await addRes.json();
|
|
386
386
|
if (!addRes.ok) { alert('Failed: ' + (addData.error || 'unknown')); return; }
|
|
387
|
+
const addedProject = {
|
|
388
|
+
name: addData.name,
|
|
389
|
+
description: (addData.detected && addData.detected.description) || '',
|
|
390
|
+
path: addData.path,
|
|
391
|
+
localPath: addData.path,
|
|
392
|
+
};
|
|
393
|
+
if (_settingsData && Array.isArray(_settingsData.projects)) {
|
|
394
|
+
const exists = _settingsData.projects.some(function(p) {
|
|
395
|
+
return p.name === addedProject.name || String(p.localPath || p.path || '').replace(/\\/g, '/') === String(addedProject.localPath || '').replace(/\\/g, '/');
|
|
396
|
+
});
|
|
397
|
+
if (!exists) _settingsData.projects = _settingsData.projects.concat([addedProject]);
|
|
398
|
+
}
|
|
399
|
+
if (typeof optimisticallyAddProject === 'function') optimisticallyAddProject(addedProject);
|
|
387
400
|
try { showToast('cmd-toast', 'Project "' + addData.name + '" added — restart engine to pick it up', true); } catch { /* expected */ }
|
|
388
401
|
refresh();
|
|
389
402
|
} catch (e) { alert('Error: ' + e.message); }
|
package/dashboard.js
CHANGED
|
@@ -3807,6 +3807,17 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3807
3807
|
return jsonReply(res, 200, { confirmToken: token, ttlMs: PROJECT_CONFIRM_TOKEN_TTL_MS });
|
|
3808
3808
|
}
|
|
3809
3809
|
|
|
3810
|
+
function _execGitInRepo(repoPath, args, timeoutMs) {
|
|
3811
|
+
const { execFileSync } = require('child_process');
|
|
3812
|
+
return execFileSync('git', args, {
|
|
3813
|
+
cwd: repoPath,
|
|
3814
|
+
encoding: 'utf8',
|
|
3815
|
+
timeout: timeoutMs || 5000,
|
|
3816
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
3817
|
+
windowsHide: true,
|
|
3818
|
+
}).trim();
|
|
3819
|
+
}
|
|
3820
|
+
|
|
3810
3821
|
async function handleProjectsAdd(req, res) {
|
|
3811
3822
|
try {
|
|
3812
3823
|
const body = await readBody(req);
|
|
@@ -3840,14 +3851,16 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3840
3851
|
}
|
|
3841
3852
|
|
|
3842
3853
|
// Auto-discover from git repo
|
|
3843
|
-
const { execSync: ex } = require('child_process');
|
|
3844
3854
|
const detected = { name: path.basename(target), _found: [] };
|
|
3845
3855
|
try {
|
|
3846
|
-
|
|
3856
|
+
let head = '';
|
|
3857
|
+
try { head = _execGitInRepo(target, ['symbolic-ref', 'refs/remotes/origin/HEAD'], 5000); }
|
|
3858
|
+
catch { head = _execGitInRepo(target, ['symbolic-ref', 'HEAD'], 5000); }
|
|
3859
|
+
if (!head) throw new Error('empty git ref');
|
|
3847
3860
|
detected.mainBranch = head.replace('refs/remotes/origin/', '').replace('refs/heads/', '');
|
|
3848
3861
|
} catch { detected.mainBranch = 'main'; }
|
|
3849
3862
|
try {
|
|
3850
|
-
const remoteUrl =
|
|
3863
|
+
const remoteUrl = _execGitInRepo(target, ['remote', 'get-url', 'origin'], 5000);
|
|
3851
3864
|
if (remoteUrl.includes('github.com')) {
|
|
3852
3865
|
detected.repoHost = 'github';
|
|
3853
3866
|
const m = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
|
|
@@ -3906,6 +3919,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3906
3919
|
config.projects.push(project);
|
|
3907
3920
|
safeWrite(configPath, config);
|
|
3908
3921
|
reloadConfig(); // Update in-memory project list immediately
|
|
3922
|
+
invalidateStatusCache();
|
|
3909
3923
|
|
|
3910
3924
|
// Create project-local state files
|
|
3911
3925
|
const minionsDir = path.join(target, '.minions');
|
|
@@ -3969,7 +3983,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3969
3983
|
const results = repos.map(repoPath => {
|
|
3970
3984
|
const result = { path: repoPath.replace(/\\/g, '/'), name: path.basename(repoPath), host: 'git', linked: existingPaths.has(path.resolve(repoPath)) };
|
|
3971
3985
|
try {
|
|
3972
|
-
const remoteUrl =
|
|
3986
|
+
const remoteUrl = _execGitInRepo(repoPath, ['remote', 'get-url', 'origin'], 3000);
|
|
3973
3987
|
const gh = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
|
|
3974
3988
|
const ado = remoteUrl.match(/dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/([^/\s]+)/) || remoteUrl.match(/([^.]+)\.visualstudio\.com.*?\/([^/]+)\/_git\/([^/\s]+)/);
|
|
3975
3989
|
if (gh) { result.host = 'GitHub'; result.org = gh[1]; result.name = gh[2]; }
|
package/engine/lifecycle.js
CHANGED
|
@@ -892,6 +892,23 @@ function parseReviewVerdict(text) {
|
|
|
892
892
|
return null;
|
|
893
893
|
}
|
|
894
894
|
|
|
895
|
+
/**
|
|
896
|
+
* Detect "idempotent bailout" output from a review agent — the agent saw a
|
|
897
|
+
* prior review on the PR (or the same dispatchKey re-fired) and chose to bail
|
|
898
|
+
* rather than spam a duplicate comment.
|
|
899
|
+
*
|
|
900
|
+
* Such output is intentionally short and contains no VERDICT keyword. Treating
|
|
901
|
+
* it as a retryable failure burns _retryCount and eventually flips the WI to
|
|
902
|
+
* status=failed even though the original review was successfully posted (#1770).
|
|
903
|
+
*
|
|
904
|
+
* @param {string} text - Agent output / resultSummary
|
|
905
|
+
* @returns {boolean}
|
|
906
|
+
*/
|
|
907
|
+
function isReviewBailout(text) {
|
|
908
|
+
if (!text || typeof text !== 'string') return false;
|
|
909
|
+
return /bail(ing)?\s+out/i.test(text) || /already\s+posted/i.test(text);
|
|
910
|
+
}
|
|
911
|
+
|
|
895
912
|
async function updatePrAfterReview(agentId, pr, project, config, resultSummary) {
|
|
896
913
|
|
|
897
914
|
if (!pr?.id) return;
|
|
@@ -1657,9 +1674,18 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
1657
1674
|
// same pattern as plan-to-prd (#893): updateWorkItemStatus deletes _retryCount, so the check
|
|
1658
1675
|
// must read/increment it before that happens. Also sets skipDoneStatus so completedAt isn't
|
|
1659
1676
|
// written and then left dangling when status is reset to pending for retry.
|
|
1677
|
+
//
|
|
1678
|
+
// (#1770) Idempotent bailout: if the agent explicitly bailed because a review was
|
|
1679
|
+
// already posted (e.g. the WI got re-dispatched before lifecycle marked the first
|
|
1680
|
+
// run done), treat the run as success — fall through to mark DONE without retry.
|
|
1681
|
+
// Without this, the second run produces no VERDICT, _retryCount increments,
|
|
1682
|
+
// and after 3 such bailouts the WI flips to status=failed even though the
|
|
1683
|
+
// original review was posted on the first run.
|
|
1660
1684
|
if (effectiveSuccess && type === WORK_TYPE.REVIEW && meta?.item?.id) {
|
|
1661
1685
|
const verdict = parseReviewVerdict(resultSummary);
|
|
1662
|
-
if (!verdict) {
|
|
1686
|
+
if (!verdict && isReviewBailout(resultSummary)) {
|
|
1687
|
+
log('info', `Review ${meta.item.id} bailed out (review already posted) — treating as DONE without retry`);
|
|
1688
|
+
} else if (!verdict) {
|
|
1663
1689
|
skipDoneStatus = true;
|
|
1664
1690
|
const wiPath = resolveWorkItemPath(meta);
|
|
1665
1691
|
if (wiPath) {
|
|
@@ -1672,6 +1698,8 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
1672
1698
|
if (retries < ENGINE_DEFAULTS.maxRetries) {
|
|
1673
1699
|
w.status = WI_STATUS.PENDING;
|
|
1674
1700
|
w._retryCount = retries + 1;
|
|
1701
|
+
w._lastRetryAt = ts();
|
|
1702
|
+
w._lastRetryReason = 'no review verdict';
|
|
1675
1703
|
delete w.dispatched_at;
|
|
1676
1704
|
delete w.completedAt;
|
|
1677
1705
|
log('warn', `Review ${meta.item.id} completed without verdict — auto-retry ${retries + 1}/${ENGINE_DEFAULTS.maxRetries}`);
|
|
@@ -2162,6 +2190,7 @@ module.exports = {
|
|
|
2162
2190
|
updateMetrics,
|
|
2163
2191
|
parseAgentOutput,
|
|
2164
2192
|
parseReviewVerdict,
|
|
2193
|
+
isReviewBailout,
|
|
2165
2194
|
parseStructuredCompletion,
|
|
2166
2195
|
runPostCompletionHooks,
|
|
2167
2196
|
syncPrdFromPrs,
|
package/engine/shared.js
CHANGED
|
@@ -758,6 +758,7 @@ const ENGINE_DEFAULTS = {
|
|
|
758
758
|
evalMaxIterations: 3, // max review→fix cycles before escalating to human
|
|
759
759
|
evalMaxCost: null, // USD ceiling per work item across all eval iterations; null = no limit (gather baseline data first)
|
|
760
760
|
maxRetries: 3, // max dispatch retries before marking work item as failed
|
|
761
|
+
minRetryGapMs: 120000, // 2min — minimum gap between retry dispatches for the same work item; prevents tight retry loops when an idempotent agent (e.g. review bailing out on a duplicate) cannot produce the expected output (#1770)
|
|
761
762
|
pipelineApiRetries: 2, // max attempts for pipeline API calls
|
|
762
763
|
pipelineApiRetryDelay: 2000, // ms delay between pipeline API retries
|
|
763
764
|
versionCheckInterval: 3600000, // 1 hour — how often to check npm for updates (ms)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1569",
|
|
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"
|