@yemi33/minions 0.1.740 → 0.1.742
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 +5 -1
- package/engine/lifecycle.js +25 -9
- package/engine/shared.js +59 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.742 (2026-04-09)
|
|
4
4
|
|
|
5
5
|
### Features
|
|
6
6
|
- add prNumber field to pull-requests.json records (#711)
|
|
7
7
|
|
|
8
|
+
### Fixes
|
|
9
|
+
- escalate failed plan items instead of blocking indefinitely (closes #722) (#733)
|
|
10
|
+
- handle NUL pseudo-file in Windows worktree cleanup (#731)
|
|
11
|
+
|
|
8
12
|
## 0.1.739 (2026-04-09)
|
|
9
13
|
|
|
10
14
|
### Fixes
|
package/engine/lifecycle.js
CHANGED
|
@@ -8,7 +8,7 @@ const path = require('path');
|
|
|
8
8
|
const os = require('os');
|
|
9
9
|
const shared = require('./shared');
|
|
10
10
|
const { safeRead, safeJson, safeWrite, mutateJsonFileLocked, mutateWorkItems, execSilent, projectPrPath, getPrLinks, addPrLink,
|
|
11
|
-
log, ts, dateStamp, WI_STATUS, DONE_STATUSES, WORK_TYPE, PLAN_STATUS, PR_STATUS, DISPATCH_RESULT,
|
|
11
|
+
log, ts, dateStamp, WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, WORK_TYPE, PLAN_STATUS, PR_STATUS, DISPATCH_RESULT,
|
|
12
12
|
ENGINE_DEFAULTS, DEFAULT_AGENT_METRICS, FAILURE_CLASS } = shared;
|
|
13
13
|
const { trackEngineUsage } = require('./llm');
|
|
14
14
|
const queries = require('./queries');
|
|
@@ -34,7 +34,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
34
34
|
const planItems = allWorkItems.filter(w => w.sourcePlan === planFile && w.itemType !== 'pr' && w.itemType !== 'verify');
|
|
35
35
|
if (planItems.length === 0) return;
|
|
36
36
|
|
|
37
|
-
// Hard completion gate: every PRD feature ID must have a corresponding work item in
|
|
37
|
+
// Hard completion gate: every PRD feature ID must have a corresponding work item in a terminal state.
|
|
38
38
|
const planFeatureIds = new Set((plan.missing_features || []).map(f => f.id).filter(Boolean));
|
|
39
39
|
const workItemById = {};
|
|
40
40
|
for (const w of planItems) { if (w.id) workItemById[w.id] = w; }
|
|
@@ -51,20 +51,36 @@ function checkPlanCompletion(meta, config) {
|
|
|
51
51
|
return;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
// Check 2: every feature
|
|
55
|
-
|
|
54
|
+
// Check 2: every feature must be in a terminal state (done, failed, or cancelled).
|
|
55
|
+
// Failed/cancelled items are unrecoverable — waiting on them blocks the plan indefinitely.
|
|
56
|
+
const notTerminal = [...planFeatureIds].filter(id => {
|
|
56
57
|
const w = workItemById[id];
|
|
57
|
-
if (w &&
|
|
58
|
+
if (w && PLAN_TERMINAL_STATUSES.has(w.status)) return false;
|
|
58
59
|
const prdItem = (plan.missing_features || []).find(f => f.id === id);
|
|
59
|
-
return !(prdItem &&
|
|
60
|
+
return !(prdItem && PLAN_TERMINAL_STATUSES.has(prdItem.status));
|
|
60
61
|
});
|
|
61
|
-
if (
|
|
62
|
-
log('info', `Plan ${planFile}: waiting for
|
|
62
|
+
if (notTerminal.length > 0) {
|
|
63
|
+
log('info', `Plan ${planFile}: waiting for ${notTerminal.length}/${planFeatureIds.size} item(s) to reach terminal state: ${notTerminal.join(', ')}`);
|
|
63
64
|
return;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
const doneItems = planItems.filter(w => DONE_STATUSES.has(w.status));
|
|
67
|
-
const failedItems = planItems.filter(w => w.status === WI_STATUS.FAILED);
|
|
68
|
+
const failedItems = planItems.filter(w => w.status === WI_STATUS.FAILED || w.status === WI_STATUS.CANCELLED);
|
|
69
|
+
|
|
70
|
+
// Escalate failed/cancelled items to human — write inbox alert (deduped by slug)
|
|
71
|
+
if (failedItems.length > 0) {
|
|
72
|
+
const alertSlug = `plan-failure-escalation-${planFile.replace('.json', '')}`;
|
|
73
|
+
const failDetails = failedItems.map(w =>
|
|
74
|
+
`- \`${w.id}\`: ${w.title || 'Unknown'} — ${w.failReason || w.status}`
|
|
75
|
+
).join('\n');
|
|
76
|
+
shared.writeToInbox('engine', alertSlug,
|
|
77
|
+
`# Plan Items Failed: ${plan.plan_summary || planFile}\n\n` +
|
|
78
|
+
`**${failedItems.length}** of ${planFeatureIds.size} item(s) failed or were cancelled:\n\n${failDetails}\n\n` +
|
|
79
|
+
`The plan is completing with partial results (${doneItems.length} done, ${failedItems.length} failed).\n` +
|
|
80
|
+
`Review failed items and re-dispatch manually if needed.\n`
|
|
81
|
+
);
|
|
82
|
+
log('warn', `Plan ${planFile}: ${failedItems.length} item(s) failed/cancelled — escalating to human: ${failedItems.map(w => w.id).join(', ')}`);
|
|
83
|
+
}
|
|
68
84
|
|
|
69
85
|
// 1. Mark plan as completed
|
|
70
86
|
plan.status = PLAN_STATUS.COMPLETED;
|
package/engine/shared.js
CHANGED
|
@@ -562,6 +562,9 @@ const WI_STATUS = {
|
|
|
562
562
|
// Read-side: accept legacy aliases for backward compat with old data/clients.
|
|
563
563
|
// Write-side: only WI_STATUS.DONE is written (cleanup.js migrates old values on each run).
|
|
564
564
|
const DONE_STATUSES = new Set([WI_STATUS.DONE, 'in-pr', 'implemented', 'complete']);
|
|
565
|
+
// Terminal statuses for plan completion — item won't progress further (done, failed, cancelled).
|
|
566
|
+
// Used by checkPlanCompletion to unblock the gate when items are in an unrecoverable state.
|
|
567
|
+
const PLAN_TERMINAL_STATUSES = new Set([...DONE_STATUSES, WI_STATUS.FAILED, WI_STATUS.CANCELLED]);
|
|
565
568
|
const WORK_TYPE = {
|
|
566
569
|
IMPLEMENT: 'implement', IMPLEMENT_LARGE: 'implement:large', FIX: 'fix', REVIEW: 'review',
|
|
567
570
|
VERIFY: 'verify', PLAN: 'plan', PLAN_TO_PRD: 'plan-to-prd', DECOMPOSE: 'decompose',
|
|
@@ -857,8 +860,49 @@ function mutatePullRequests(filePath, mutator) {
|
|
|
857
860
|
* Remove a git worktree, falling back to fs.rmSync if git fails (e.g., locked on Windows).
|
|
858
861
|
* Only removes directories under worktreeRoot to prevent accidental deletion.
|
|
859
862
|
* Tracks persistent failures to avoid retrying locked paths every cleanup cycle.
|
|
863
|
+
*
|
|
864
|
+
* On Windows, reserved device-name files (NUL, CON, PRN, AUX, etc.) can appear in
|
|
865
|
+
* worktree directories when shell redirections run under Git Bash/WSL. These block
|
|
866
|
+
* git worktree remove, fs.rmSync, and PowerShell Remove-Item. Two mitigations:
|
|
867
|
+
* 1. _purgeReservedFiles() deletes them via the \\?\ extended path prefix before removal
|
|
868
|
+
* 2. cmd /c rd /s /q as final fallback handles any remaining reserved names
|
|
860
869
|
*/
|
|
861
870
|
const _removeWorktreeFailures = new Map(); // path → { count, lastAttempt }
|
|
871
|
+
|
|
872
|
+
// Windows reserved device names that cannot be deleted via normal paths
|
|
873
|
+
const _WIN_RESERVED_NAMES = new Set([
|
|
874
|
+
'CON', 'PRN', 'AUX', 'NUL',
|
|
875
|
+
'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
|
|
876
|
+
'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9',
|
|
877
|
+
]);
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Recursively purge Windows reserved-name pseudo-files (NUL, CON, PRN, AUX, etc.)
|
|
881
|
+
* using the \\?\ extended path prefix that bypasses reserved-name interpretation.
|
|
882
|
+
* Called before normal deletion attempts on Windows to unblock git/fs operations.
|
|
883
|
+
*/
|
|
884
|
+
function _purgeReservedFiles(dirPath) {
|
|
885
|
+
let entries;
|
|
886
|
+
try { entries = fs.readdirSync(dirPath, { withFileTypes: true }); } catch { return; }
|
|
887
|
+
for (const entry of entries) {
|
|
888
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
889
|
+
try {
|
|
890
|
+
if (entry.isDirectory()) {
|
|
891
|
+
_purgeReservedFiles(fullPath);
|
|
892
|
+
} else {
|
|
893
|
+
// Match NUL, NUL.txt, con, con.log, etc.
|
|
894
|
+
const baseName = entry.name.toUpperCase().split('.')[0];
|
|
895
|
+
if (_WIN_RESERVED_NAMES.has(baseName)) {
|
|
896
|
+
// \\?\ prefix bypasses Win32 reserved-name interpretation
|
|
897
|
+
fs.unlinkSync('\\\\?\\' + fullPath);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
} catch {
|
|
901
|
+
// Best-effort: file may already be gone or inaccessible
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
862
906
|
function removeWorktree(wtPath, gitRoot, worktreeRoot) {
|
|
863
907
|
const resolved = path.resolve(wtPath);
|
|
864
908
|
const resolvedRoot = path.resolve(worktreeRoot) + path.sep;
|
|
@@ -870,6 +914,11 @@ function removeWorktree(wtPath, gitRoot, worktreeRoot) {
|
|
|
870
914
|
const prior = _removeWorktreeFailures.get(resolved);
|
|
871
915
|
if (prior && prior.count >= 3 && Date.now() - prior.lastAttempt < 3600000) return false;
|
|
872
916
|
|
|
917
|
+
// Windows: purge reserved-name pseudo-files (NUL, CON, etc.) that block normal deletion
|
|
918
|
+
if (process.platform === 'win32') {
|
|
919
|
+
_purgeReservedFiles(resolved);
|
|
920
|
+
}
|
|
921
|
+
|
|
873
922
|
try {
|
|
874
923
|
exec(`git worktree remove "${wtPath}" --force`, { cwd: gitRoot, stdio: 'pipe', timeout: 15000, windowsHide: true });
|
|
875
924
|
_removeWorktreeFailures.delete(resolved);
|
|
@@ -881,14 +930,17 @@ function removeWorktree(wtPath, gitRoot, worktreeRoot) {
|
|
|
881
930
|
_removeWorktreeFailures.delete(resolved);
|
|
882
931
|
return true;
|
|
883
932
|
} catch (rmErr) {
|
|
884
|
-
// Windows
|
|
885
|
-
|
|
933
|
+
// Windows: try cmd /c rd /s /q for any error — handles reserved device names,
|
|
934
|
+
// locked files, and partially-deleted directories (not just EPERM)
|
|
935
|
+
if (process.platform === 'win32') {
|
|
886
936
|
try {
|
|
887
|
-
exec(`rd /s /q "${resolved}"`, { stdio: 'pipe', timeout: 15000, windowsHide: true });
|
|
937
|
+
exec(`cmd /c rd /s /q "${resolved}"`, { stdio: 'pipe', timeout: 15000, windowsHide: true });
|
|
888
938
|
try { exec('git worktree prune', { cwd: gitRoot, stdio: 'pipe', timeout: 10000, windowsHide: true }); } catch {}
|
|
889
939
|
_removeWorktreeFailures.delete(resolved);
|
|
890
940
|
return true;
|
|
891
|
-
} catch {
|
|
941
|
+
} catch (rdErr) {
|
|
942
|
+
log('warn', `removeWorktree: rd /s /q fallback failed for ${wtPath}: ${rdErr.message}`);
|
|
943
|
+
}
|
|
892
944
|
}
|
|
893
945
|
const fail = _removeWorktreeFailures.get(resolved) || { count: 0, lastAttempt: 0 };
|
|
894
946
|
fail.count++;
|
|
@@ -940,7 +992,7 @@ module.exports = {
|
|
|
940
992
|
KB_CATEGORIES,
|
|
941
993
|
classifyInboxItem,
|
|
942
994
|
ENGINE_DEFAULTS,
|
|
943
|
-
WI_STATUS, DONE_STATUSES, WORK_TYPE, PLAN_STATUS, PR_STATUS, DISPATCH_RESULT,
|
|
995
|
+
WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, WORK_TYPE, PLAN_STATUS, PR_STATUS, DISPATCH_RESULT,
|
|
944
996
|
PIPELINE_STATUS, STAGE_TYPE, MEETING_STATUS, AGENT_STATUS,
|
|
945
997
|
FAILURE_CLASS, ESCALATION_POLICY, COMPLETION_FIELDS,
|
|
946
998
|
DEFAULT_AGENT_METRICS,
|
|
@@ -963,6 +1015,8 @@ module.exports = {
|
|
|
963
1015
|
killGracefully,
|
|
964
1016
|
killImmediate,
|
|
965
1017
|
removeWorktree,
|
|
1018
|
+
_purgeReservedFiles, // exported for testing
|
|
1019
|
+
_WIN_RESERVED_NAMES, // exported for testing
|
|
966
1020
|
LOCK_STALE_MS,
|
|
967
1021
|
flushLogs,
|
|
968
1022
|
slugify,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.742",
|
|
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"
|