@yemi33/minions 0.1.526 → 0.1.528
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 -1
- package/dashboard/js/render-work-items.js +4 -4
- package/dashboard.js +3 -3
- package/engine/cleanup.js +20 -26
- package/engine/cli.js +56 -59
- package/engine/dispatch.js +18 -30
- package/engine/lifecycle.js +38 -61
- package/engine/pipeline.js +41 -41
- package/engine/playbook.js +1 -1
- package/engine/shared.js +30 -0
- package/engine.js +7 -28
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.528 (2026-04-07)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
- Convert engine/ safeWrite calls on work-items.json to mutateWorkItems() (#415)
|
|
7
|
+
|
|
8
|
+
## 0.1.527 (2026-04-07)
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
- Replace direct safeJson reads of work-items.json with getWorkItems() (#424)
|
|
12
|
+
|
|
3
13
|
## 0.1.526 (2026-04-07)
|
|
4
14
|
|
|
5
15
|
### Features
|
|
6
|
-
- Replace new Date().toISOString() with ts() from shared.js (#411)
|
|
7
16
|
- Add Concurrency & Lock Ordering section to CLAUDE.md (#409)
|
|
8
17
|
- 9 test isolation verification tests — run last to detect pollution
|
|
9
18
|
|
|
@@ -175,8 +175,8 @@ async function submitWorkItemEdit(id, source, e) {
|
|
|
175
175
|
const refsRaw = document.getElementById('wi-edit-refs')?.value || '';
|
|
176
176
|
const references = refsRaw.split('\n').filter(function(l) { return l.trim(); }).map(function(l) {
|
|
177
177
|
var parts = l.split('|').map(function(s) { return s.trim(); });
|
|
178
|
-
return { url: parts[0], title: parts[1] || parts[0], type: parts[2] || 'link' };
|
|
179
|
-
});
|
|
178
|
+
return { url: parts[0] || '', title: parts[1] || parts[0] || '', type: parts[2] || 'link' };
|
|
179
|
+
}).filter(function(r) { return r.url; });
|
|
180
180
|
const acRaw = document.getElementById('wi-edit-ac')?.value || '';
|
|
181
181
|
const acceptanceCriteria = acRaw.split('\n').filter(function(l) { return l.trim(); });
|
|
182
182
|
if (!title) { if (btn) { btn.disabled = false; btn.textContent = 'Save'; } alert('Title is required'); return; }
|
|
@@ -387,8 +387,8 @@ async function _submitCreateWorkItem(e) {
|
|
|
387
387
|
const refsRaw = document.getElementById('wi-new-refs')?.value || '';
|
|
388
388
|
const references = refsRaw.split('\n').filter(l => l.trim()).map(l => {
|
|
389
389
|
const parts = l.split('|').map(s => s.trim());
|
|
390
|
-
return { url: parts[0], title: parts[1] || parts[0], type: parts[2] || 'link' };
|
|
391
|
-
});
|
|
390
|
+
return { url: parts[0] || '', title: parts[1] || parts[0] || '', type: parts[2] || 'link' };
|
|
391
|
+
}).filter(r => r.url);
|
|
392
392
|
|
|
393
393
|
try {
|
|
394
394
|
const body = { title, description: desc, type, priority };
|
package/dashboard.js
CHANGED
|
@@ -192,8 +192,8 @@ async function checkNpmVersion() {
|
|
|
192
192
|
_npmVersionCache = { latest: version || null, checkedAt: new Date().toISOString() };
|
|
193
193
|
_npmVersionCacheTs = now;
|
|
194
194
|
} catch (e) {
|
|
195
|
-
console.error('[version-check] npm view failed:', e.message?.split('\n')[0]);
|
|
196
|
-
_npmVersionCache = _npmVersionCache || { latest: null, checkedAt: null, error: e.message?.split('\n')[0] || 'check failed' };
|
|
195
|
+
console.error('[version-check] npm view failed:', e.message?.split('\n')?.[0]);
|
|
196
|
+
_npmVersionCache = _npmVersionCache || { latest: null, checkedAt: null, error: e.message?.split('\n')?.[0] || 'check failed' };
|
|
197
197
|
}
|
|
198
198
|
return _npmVersionCache;
|
|
199
199
|
}
|
|
@@ -3816,7 +3816,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3816
3816
|
// Determine project
|
|
3817
3817
|
reloadConfig();
|
|
3818
3818
|
const projects = shared.getProjects(CONFIG);
|
|
3819
|
-
const targetProject = projectName ? projects.find(p => p.name?.toLowerCase() === projectName.toLowerCase()) : projects[0];
|
|
3819
|
+
const targetProject = projectName ? projects.find(p => p.name?.toLowerCase() === projectName.toLowerCase()) : (projects[0] || null);
|
|
3820
3820
|
const prPath = targetProject ? shared.projectPrPath(targetProject) : path.join(MINIONS_DIR, 'pull-requests.json');
|
|
3821
3821
|
|
|
3822
3822
|
// Extract PR number from URL
|
package/engine/cleanup.js
CHANGED
|
@@ -9,7 +9,7 @@ const shared = require('./shared');
|
|
|
9
9
|
const queries = require('./queries');
|
|
10
10
|
|
|
11
11
|
const { exec, execSilent, log, ts } = shared;
|
|
12
|
-
const { safeJson, safeWrite, safeReadDir, getProjects, projectWorkItemsPath, projectPrPath,
|
|
12
|
+
const { safeJson, safeWrite, safeReadDir, mutateWorkItems, getProjects, projectWorkItemsPath, projectPrPath,
|
|
13
13
|
sanitizeBranch, KB_CATEGORIES } = shared;
|
|
14
14
|
const { getDispatch, getAgentStatus } = queries;
|
|
15
15
|
|
|
@@ -308,15 +308,9 @@ function runCleanup(config, verbose = false) {
|
|
|
308
308
|
// Collect all work item IDs across all sources
|
|
309
309
|
const allWiIds = new Set();
|
|
310
310
|
try {
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
} catch (e) { log('warn', 'read
|
|
314
|
-
for (const project of projects) {
|
|
315
|
-
try {
|
|
316
|
-
const projItems = safeJson(projectWorkItemsPath(project)) || [];
|
|
317
|
-
projItems.forEach(w => allWiIds.add(w.id));
|
|
318
|
-
} catch (e) { log('warn', 'read project work items for orphan check: ' + e.message); }
|
|
319
|
-
}
|
|
311
|
+
const allItems = queries.getWorkItems();
|
|
312
|
+
allItems.forEach(w => allWiIds.add(w.id));
|
|
313
|
+
} catch (e) { log('warn', 'read work items for orphan check: ' + e.message); }
|
|
320
314
|
|
|
321
315
|
let changed = false;
|
|
322
316
|
for (const queue of ['pending', 'active']) {
|
|
@@ -460,17 +454,17 @@ function runCleanup(config, verbose = false) {
|
|
|
460
454
|
for (const project of projects) {
|
|
461
455
|
try {
|
|
462
456
|
const wiPath = projectWorkItemsPath(project);
|
|
463
|
-
const items = safeJson(wiPath) || [];
|
|
464
457
|
let migrated = 0;
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
item.status
|
|
468
|
-
|
|
469
|
-
|
|
458
|
+
mutateWorkItems(wiPath, items => {
|
|
459
|
+
for (const item of items) {
|
|
460
|
+
if (LEGACY_DONE_ALIASES.has(item.status)) {
|
|
461
|
+
item.status = shared.WI_STATUS.DONE;
|
|
462
|
+
delete item._pendingReason;
|
|
463
|
+
migrated++;
|
|
464
|
+
}
|
|
470
465
|
}
|
|
471
|
-
}
|
|
466
|
+
});
|
|
472
467
|
if (migrated > 0) {
|
|
473
|
-
safeWrite(wiPath, items);
|
|
474
468
|
log('info', `Migrated ${migrated} legacy status(es) → done in ${project.name} work items`);
|
|
475
469
|
}
|
|
476
470
|
} catch (e) { log('warn', 'migrate legacy statuses: ' + e.message); }
|
|
@@ -478,17 +472,17 @@ function runCleanup(config, verbose = false) {
|
|
|
478
472
|
// Central work items
|
|
479
473
|
try {
|
|
480
474
|
const centralPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
481
|
-
const centralItems = safeJson(centralPath) || [];
|
|
482
475
|
let migrated = 0;
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
item.status
|
|
486
|
-
|
|
487
|
-
|
|
476
|
+
mutateWorkItems(centralPath, items => {
|
|
477
|
+
for (const item of items) {
|
|
478
|
+
if (LEGACY_DONE_ALIASES.has(item.status)) {
|
|
479
|
+
item.status = shared.WI_STATUS.DONE;
|
|
480
|
+
delete item._pendingReason;
|
|
481
|
+
migrated++;
|
|
482
|
+
}
|
|
488
483
|
}
|
|
489
|
-
}
|
|
484
|
+
});
|
|
490
485
|
if (migrated > 0) {
|
|
491
|
-
safeWrite(centralPath, centralItems);
|
|
492
486
|
log('info', `Migrated ${migrated} legacy status(es) → done in central work items`);
|
|
493
487
|
}
|
|
494
488
|
} catch (e) { log('warn', 'migrate central legacy statuses: ' + e.message); }
|
package/engine/cli.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, ts, WI_STATUS, WORK_TYPE, PLAN_STATUS, PR_STATUS, DISPATCH_RESULT } = shared;
|
|
9
|
+
const { safeRead, safeJson, safeWrite, mutateWorkItems, ts, WI_STATUS, WORK_TYPE, PLAN_STATUS, PR_STATUS, DISPATCH_RESULT } = shared;
|
|
10
10
|
const queries = require('./queries');
|
|
11
11
|
const { getConfig, getControl, getDispatch, getAgentStatus,
|
|
12
12
|
MINIONS_DIR, ENGINE_DIR, AGENTS_DIR, PLANS_DIR, PRD_DIR, CONTROL_PATH, DISPATCH_PATH } = queries;
|
|
@@ -165,18 +165,18 @@ const commands = {
|
|
|
165
165
|
if (sj?.sessionId) sessionId = sj.sessionId;
|
|
166
166
|
} catch {}
|
|
167
167
|
e.activeProcesses.set(item.id, { proc: { pid: agentPid > 0 ? agentPid : null }, agentId, startedAt: item.created_at, reattached: true, sessionId });
|
|
168
|
-
// Sync work item status to dispatched —
|
|
168
|
+
// Sync work item status to dispatched — atomic write to avoid lifecycle lazy init issues
|
|
169
169
|
if (item.meta?.item?.id && item.meta?.project?.localPath) {
|
|
170
170
|
try {
|
|
171
171
|
const wiPath = path.join(MINIONS_DIR, "projects", item.meta.project.name, "work-items.json");
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
172
|
+
mutateWorkItems(wiPath, items => {
|
|
173
|
+
const wi = items.find(w => w.id === item.meta.item.id);
|
|
174
|
+
if (wi && wi.status !== WI_STATUS.DISPATCHED) {
|
|
175
|
+
wi.status = WI_STATUS.DISPATCHED;
|
|
176
|
+
wi.dispatched_to = wi.dispatched_to || agentId;
|
|
177
|
+
wi.dispatched_at = wi.dispatched_at || ts();
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
180
|
} catch (err) { console.log(` Warning: failed to sync work item status: ${err.message}`); }
|
|
181
181
|
}
|
|
182
182
|
reattached++;
|
|
@@ -264,19 +264,19 @@ const commands = {
|
|
|
264
264
|
try {
|
|
265
265
|
lifecycle.updateWorkItemStatus(item.meta, status, isSuccess ? '' : 'Completed while engine was down');
|
|
266
266
|
} catch {
|
|
267
|
-
//
|
|
267
|
+
// Atomic file write fallback
|
|
268
268
|
try {
|
|
269
269
|
const projName = item.meta.project?.name;
|
|
270
270
|
if (projName) {
|
|
271
271
|
const wiPath = path.join(MINIONS_DIR, 'projects', projName, 'work-items.json');
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
272
|
+
mutateWorkItems(wiPath, items => {
|
|
273
|
+
const wi = items.find(w => w.id === item.meta.item.id);
|
|
274
|
+
if (wi) {
|
|
275
|
+
wi.status = status;
|
|
276
|
+
if (isSuccess) { wi.completedAt = ts(); delete wi.failReason; }
|
|
277
|
+
else { wi.failedAt = ts(); wi.failReason = 'Completed while engine was down'; }
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
280
|
}
|
|
281
281
|
} catch (err) { e.log('warn', `Orphan WI fallback: ${err.message}`); }
|
|
282
282
|
}
|
|
@@ -321,19 +321,17 @@ const commands = {
|
|
|
321
321
|
}
|
|
322
322
|
for (const wiPath of allWiPaths) {
|
|
323
323
|
try {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
e.log('info', `Recovery: reset stuck item ${item.id} from dispatched → pending`);
|
|
324
|
+
mutateWorkItems(wiPath, items => {
|
|
325
|
+
for (const item of items) {
|
|
326
|
+
if (item.status === WI_STATUS.DISPATCHED && !activeIds.has(item.id)) {
|
|
327
|
+
item.status = WI_STATUS.PENDING;
|
|
328
|
+
delete item.dispatched_at;
|
|
329
|
+
delete item.dispatched_to;
|
|
330
|
+
fixes++;
|
|
331
|
+
e.log('info', `Recovery: reset stuck item ${item.id} from dispatched → pending`);
|
|
332
|
+
}
|
|
334
333
|
}
|
|
335
|
-
}
|
|
336
|
-
if (changed) safeWrite(wiPath, items);
|
|
334
|
+
});
|
|
337
335
|
} catch (err) { e.log('warn', `Recovery WI reset: ${err.message}`); }
|
|
338
336
|
}
|
|
339
337
|
|
|
@@ -691,24 +689,23 @@ const commands = {
|
|
|
691
689
|
? projects.find(p => p.name?.toLowerCase() === opts.project?.toLowerCase()) || projects[0]
|
|
692
690
|
: projects[0];
|
|
693
691
|
const wiPath = projectWorkItemsPath(targetProject);
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
safeWrite(wiPath, items);
|
|
692
|
+
let item;
|
|
693
|
+
mutateWorkItems(wiPath, items => {
|
|
694
|
+
item = {
|
|
695
|
+
id: `W${String(items.length + 1).padStart(3, '0')}`,
|
|
696
|
+
title: title,
|
|
697
|
+
type: opts.type || 'implement',
|
|
698
|
+
status: WI_STATUS.QUEUED,
|
|
699
|
+
priority: opts.priority || 'medium',
|
|
700
|
+
complexity: opts.complexity || 'medium',
|
|
701
|
+
description: opts.description || title,
|
|
702
|
+
agent: opts.agent || null,
|
|
703
|
+
branch: opts.branch || null,
|
|
704
|
+
prompt: opts.prompt || null,
|
|
705
|
+
created_at: e.ts()
|
|
706
|
+
};
|
|
707
|
+
items.push(item);
|
|
708
|
+
});
|
|
712
709
|
|
|
713
710
|
console.log(`Queued work item: ${item.id} — ${item.title} (project: ${targetProject.name || 'default'})`);
|
|
714
711
|
console.log(` Type: ${item.type} | Priority: ${item.priority} | Agent: ${item.agent || 'auto'}`);
|
|
@@ -903,16 +900,16 @@ const commands = {
|
|
|
903
900
|
? shared.projectWorkItemsPath({ localPath: item.meta.project.localPath, name: item.meta.project.name, workSources: config.projects?.find(p => p.name === item.meta.project.name)?.workSources })
|
|
904
901
|
: null;
|
|
905
902
|
if (wiPath) {
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
}
|
|
903
|
+
mutateWorkItems(wiPath, items => {
|
|
904
|
+
const target = items.find(i => i.id === itemId);
|
|
905
|
+
if (target) {
|
|
906
|
+
target.status = WI_STATUS.PENDING;
|
|
907
|
+
delete target.dispatched_at;
|
|
908
|
+
delete target.dispatched_to;
|
|
909
|
+
delete target.failReason;
|
|
910
|
+
delete target.failedAt;
|
|
911
|
+
}
|
|
912
|
+
});
|
|
916
913
|
}
|
|
917
914
|
}
|
|
918
915
|
}
|
package/engine/dispatch.js
CHANGED
|
@@ -9,7 +9,7 @@ const shared = require('./shared');
|
|
|
9
9
|
const queries = require('./queries');
|
|
10
10
|
const { setCooldownFailure } = require('./cooldown');
|
|
11
11
|
|
|
12
|
-
const { safeJson, safeWrite, safeReadDir, mutateJsonFileLocked,
|
|
12
|
+
const { safeJson, safeWrite, safeReadDir, mutateJsonFileLocked, mutateWorkItems,
|
|
13
13
|
getProjects, projectWorkItemsPath, log, ts, dateStamp,
|
|
14
14
|
WI_STATUS, DISPATCH_RESULT, ENGINE_DEFAULTS } = shared;
|
|
15
15
|
const { getConfig, getDispatch, DISPATCH_PATH, INBOX_DIR } = queries;
|
|
@@ -126,14 +126,8 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
|
|
|
126
126
|
if (processWorkItemFailure && result === DISPATCH_RESULT.ERROR && item.meta?.item?.id) {
|
|
127
127
|
let retries = (item.meta.item._retryCount || 0);
|
|
128
128
|
try {
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
131
|
-
const items = safeJson(wiPath);
|
|
132
|
-
if (items && Array.isArray(items)) {
|
|
133
|
-
const wi = items.find(i => i.id === item.meta.item.id);
|
|
134
|
-
if (wi) retries = wi._retryCount || 0;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
129
|
+
const wi = queries.getWorkItems().find(i => i.id === item.meta.item.id);
|
|
130
|
+
if (wi) retries = wi._retryCount || 0;
|
|
137
131
|
} catch (e) { log('warn', 'read retry count: ' + e.message); }
|
|
138
132
|
const maxRetries = ENGINE_DEFAULTS.maxRetries;
|
|
139
133
|
if (retryableFailure && retries < maxRetries) {
|
|
@@ -152,20 +146,19 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
|
|
|
152
146
|
try {
|
|
153
147
|
const wiPath = lifecycle().resolveWorkItemPath(item.meta);
|
|
154
148
|
if (wiPath) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
149
|
+
mutateWorkItems(wiPath, items => {
|
|
150
|
+
const wi = items.find(i => i.id === item.meta.item.id);
|
|
151
|
+
if (wi && wi.status !== WI_STATUS.PAUSED && wi.status !== WI_STATUS.DONE && !wi.completedAt) {
|
|
152
|
+
wi._retryCount = retries + 1;
|
|
153
|
+
wi.status = WI_STATUS.PENDING;
|
|
154
|
+
wi._lastRetryReason = reason || '';
|
|
155
|
+
wi._lastRetryAt = ts();
|
|
156
|
+
delete wi.failReason;
|
|
157
|
+
delete wi.failedAt;
|
|
158
|
+
delete wi.dispatched_at;
|
|
159
|
+
delete wi.dispatched_to;
|
|
160
|
+
}
|
|
161
|
+
});
|
|
169
162
|
}
|
|
170
163
|
} catch (e) { log('warn', 'increment retry counter: ' + e.message); }
|
|
171
164
|
} else {
|
|
@@ -178,13 +171,8 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
|
|
|
178
171
|
const config = getConfig();
|
|
179
172
|
const failedId = item.meta.item.id;
|
|
180
173
|
const blockedItems = [];
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
items.filter(w => w.status === WI_STATUS.PENDING && (w.depends_on || []).includes(failedId))
|
|
184
|
-
.forEach(w => blockedItems.push(`- \`${w.id}\` — ${w.title}`));
|
|
185
|
-
}
|
|
186
|
-
const centralItems = safeJson(path.join(MINIONS_DIR, 'work-items.json')) || [];
|
|
187
|
-
centralItems.filter(w => w.status === WI_STATUS.PENDING && (w.depends_on || []).includes(failedId))
|
|
174
|
+
const allItems = queries.getWorkItems(config);
|
|
175
|
+
allItems.filter(w => w.status === WI_STATUS.PENDING && (w.depends_on || []).includes(failedId))
|
|
188
176
|
.forEach(w => blockedItems.push(`- \`${w.id}\` — ${w.title}`));
|
|
189
177
|
|
|
190
178
|
writeInboxAlert(`failed-${failedId}`,
|
package/engine/lifecycle.js
CHANGED
|
@@ -7,7 +7,7 @@ const fs = require('fs');
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const os = require('os');
|
|
9
9
|
const shared = require('./shared');
|
|
10
|
-
const { safeRead, safeJson, safeWrite, mutateJsonFileLocked, execSilent, projectPrPath, getPrLinks, addPrLink,
|
|
10
|
+
const { safeRead, safeJson, safeWrite, mutateJsonFileLocked, mutateWorkItems, execSilent, projectPrPath, getPrLinks, addPrLink,
|
|
11
11
|
log, ts, dateStamp, WI_STATUS, DONE_STATUSES, WORK_TYPE, PLAN_STATUS, PR_STATUS, DISPATCH_RESULT,
|
|
12
12
|
ENGINE_DEFAULTS } = shared;
|
|
13
13
|
const { trackEngineUsage } = require('./llm');
|
|
@@ -30,20 +30,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
30
30
|
const projects = shared.getProjects(config);
|
|
31
31
|
|
|
32
32
|
// Collect work items from ALL projects + central (PRD items can be in either)
|
|
33
|
-
|
|
34
|
-
for (const p of projects) {
|
|
35
|
-
try {
|
|
36
|
-
const wi = safeJson(shared.projectWorkItemsPath(p)) || [];
|
|
37
|
-
allWorkItems = allWorkItems.concat(wi);
|
|
38
|
-
} catch { /* optional */ }
|
|
39
|
-
}
|
|
40
|
-
// Also check central work-items.json (for no-project setups)
|
|
41
|
-
try {
|
|
42
|
-
const central = safeJson(path.join(MINIONS_DIR, 'work-items.json')) || [];
|
|
43
|
-
for (const w of central) {
|
|
44
|
-
if (!allWorkItems.some(existing => existing.id === w.id)) allWorkItems.push(w);
|
|
45
|
-
}
|
|
46
|
-
} catch { /* optional */ }
|
|
33
|
+
const allWorkItems = queries.getWorkItems(config);
|
|
47
34
|
const planItems = allWorkItems.filter(w => w.sourcePlan === planFile && w.itemType !== 'pr' && w.itemType !== 'verify');
|
|
48
35
|
if (planItems.length === 0) return;
|
|
49
36
|
|
|
@@ -158,7 +145,6 @@ function checkPlanCompletion(meta, config) {
|
|
|
158
145
|
return;
|
|
159
146
|
}
|
|
160
147
|
const wiPath = shared.projectWorkItemsPath(primaryProject);
|
|
161
|
-
const workItems = safeJson(wiPath) || [];
|
|
162
148
|
|
|
163
149
|
// 3. For shared-branch plans, create PR work item
|
|
164
150
|
if (plan.branch_strategy === 'shared-branch' && plan.feature_branch && wiPath) {
|
|
@@ -168,15 +154,16 @@ function checkPlanCompletion(meta, config) {
|
|
|
168
154
|
const featureBranch = plan.feature_branch;
|
|
169
155
|
const mainBranch = shared.resolveMainBranch(primaryProject.localPath, primaryProject.mainBranch);
|
|
170
156
|
const itemSummary = doneItems.map(w => '- ' + w.id + ': ' + w.title.replace('Implement: ', '')).join('\n');
|
|
171
|
-
workItems
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
157
|
+
mutateWorkItems(wiPath, workItems => {
|
|
158
|
+
workItems.push({
|
|
159
|
+
id, title: `Create PR for plan: ${plan.plan_summary || planFile}`,
|
|
160
|
+
type: 'implement', priority: 'high',
|
|
161
|
+
description: `All plan items from \`${planFile}\` are complete on branch \`${featureBranch}\`.\n\n**Branch:** \`${featureBranch}\`\n**Target:** \`${mainBranch}\`\n\n## Completed Items\n${itemSummary}`,
|
|
162
|
+
status: WI_STATUS.PENDING, created: ts(), createdBy: 'engine:plan-completion',
|
|
163
|
+
sourcePlan: planFile, itemType: 'pr',
|
|
164
|
+
branch: featureBranch, branchStrategy: 'shared-branch', project: projectName,
|
|
165
|
+
});
|
|
178
166
|
});
|
|
179
|
-
shared.safeWrite(wiPath, workItems);
|
|
180
167
|
}
|
|
181
168
|
}
|
|
182
169
|
|
|
@@ -257,20 +244,21 @@ function checkPlanCompletion(meta, config) {
|
|
|
257
244
|
prSummary,
|
|
258
245
|
].join('\n');
|
|
259
246
|
|
|
260
|
-
workItems
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
247
|
+
mutateWorkItems(wiPath, workItems => {
|
|
248
|
+
workItems.push({
|
|
249
|
+
id: verifyId,
|
|
250
|
+
title: `Verify plan: ${(plan.plan_summary || planFile).slice(0, 80)}`,
|
|
251
|
+
type: 'verify',
|
|
252
|
+
priority: 'high',
|
|
253
|
+
description,
|
|
254
|
+
status: WI_STATUS.PENDING,
|
|
255
|
+
created: ts(),
|
|
256
|
+
createdBy: 'engine:plan-verification',
|
|
257
|
+
sourcePlan: planFile,
|
|
258
|
+
itemType: 'verify',
|
|
259
|
+
project: projectName,
|
|
260
|
+
});
|
|
272
261
|
});
|
|
273
|
-
shared.safeWrite(wiPath, workItems);
|
|
274
262
|
log('info', `Created verification work item ${verifyId} for plan ${planFile}`);
|
|
275
263
|
}
|
|
276
264
|
|
|
@@ -330,19 +318,7 @@ function archivePlan(planFile, plan, projects, config) {
|
|
|
330
318
|
if (plan.feature_branch) branchSlugs.add(shared.sanitizeBranch(plan.feature_branch).toLowerCase());
|
|
331
319
|
|
|
332
320
|
// Collect work items for this plan
|
|
333
|
-
|
|
334
|
-
for (const p of projects) {
|
|
335
|
-
try {
|
|
336
|
-
const wi = safeJson(shared.projectWorkItemsPath(p)) || [];
|
|
337
|
-
allWorkItems = allWorkItems.concat(wi);
|
|
338
|
-
} catch { /* optional */ }
|
|
339
|
-
}
|
|
340
|
-
try {
|
|
341
|
-
const central = safeJson(path.join(MINIONS_DIR, 'work-items.json')) || [];
|
|
342
|
-
for (const w of central) {
|
|
343
|
-
if (!allWorkItems.some(existing => existing.id === w.id)) allWorkItems.push(w);
|
|
344
|
-
}
|
|
345
|
-
} catch { /* optional */ }
|
|
321
|
+
const allWorkItems = queries.getWorkItems(config);
|
|
346
322
|
const planItems = allWorkItems.filter(w => w.sourcePlan === planFile);
|
|
347
323
|
const doneItems = planItems.filter(w => DONE_STATUSES.has(w.status));
|
|
348
324
|
|
|
@@ -836,17 +812,18 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
836
812
|
for (const p of shared.getProjects(config)) wiPaths.push(shared.projectWorkItemsPath(p));
|
|
837
813
|
for (const wiPath of wiPaths) {
|
|
838
814
|
try {
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
}
|
|
815
|
+
let found = false;
|
|
816
|
+
mutateWorkItems(wiPath, items => {
|
|
817
|
+
const item = items.find(i => i.id === mergedItemId);
|
|
818
|
+
if (item && item.status !== WI_STATUS.DONE) {
|
|
819
|
+
log('info', `Post-merge: marking work item ${mergedItemId} as done (was ${item.status}) for ${pr.id}`);
|
|
820
|
+
item.status = WI_STATUS.DONE;
|
|
821
|
+
item.completedAt = ts();
|
|
822
|
+
item._mergedVia = pr.id;
|
|
823
|
+
found = true;
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
if (found) break;
|
|
850
827
|
} catch (err) { log('warn', `Post-merge work item update: ${err.message}`); }
|
|
851
828
|
}
|
|
852
829
|
}
|
package/engine/pipeline.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const shared = require('./shared');
|
|
10
|
-
const { safeJson, safeWrite, safeRead, safeReadDir, uid, log, ts, dateStamp, mutateJsonFileLocked, WI_STATUS, WORK_TYPE, PLAN_STATUS, PR_STATUS, PIPELINE_STATUS, STAGE_TYPE, MEETING_STATUS, ENGINE_DEFAULTS } = shared;
|
|
10
|
+
const { safeJson, safeWrite, safeRead, safeReadDir, uid, log, ts, dateStamp, mutateJsonFileLocked, mutateWorkItems, WI_STATUS, WORK_TYPE, PLAN_STATUS, PR_STATUS, PIPELINE_STATUS, STAGE_TYPE, MEETING_STATUS, ENGINE_DEFAULTS } = shared;
|
|
11
11
|
const http = require('http');
|
|
12
12
|
const { parseCronExpr, shouldRunNow } = require('./scheduler');
|
|
13
13
|
|
|
@@ -159,32 +159,32 @@ function executeTaskStage(stage, stageState, run, config) {
|
|
|
159
159
|
const items = stage.items || [{ title: stage.title, description: stage.description || '', type: stage.taskType || 'explore', agent: stage.agent }];
|
|
160
160
|
const count = stage.count || items.length;
|
|
161
161
|
const wiPath = path.join(__dirname, '..', 'work-items.json');
|
|
162
|
-
const workItems = safeJson(wiPath) || [];
|
|
163
162
|
const createdIds = [];
|
|
164
163
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
164
|
+
mutateWorkItems(wiPath, workItems => {
|
|
165
|
+
for (let i = 0; i < count; i++) {
|
|
166
|
+
const item = items[i % items.length];
|
|
167
|
+
const id = `PL-${run.runId.slice(4, 12)}-${stage.id}-${i}`;
|
|
168
|
+
if (workItems.some(w => w.id === id)) { createdIds.push(id); continue; }
|
|
169
|
+
workItems.push({
|
|
170
|
+
id,
|
|
171
|
+
title: item.title || stage.title,
|
|
172
|
+
description: item.description || stage.description || '',
|
|
173
|
+
type: item.type || stage.taskType || 'explore',
|
|
174
|
+
priority: item.priority || stage.priority || 'medium',
|
|
175
|
+
// Only set agent if explicitly specified — otherwise engine routing assigns any available agent
|
|
176
|
+
...(item.agent || stage.agent ? { agent: item.agent || stage.agent } : {}),
|
|
177
|
+
status: WI_STATUS.PENDING,
|
|
178
|
+
created: ts(),
|
|
179
|
+
createdBy: 'pipeline:' + run.pipelineId,
|
|
180
|
+
branch: `pipeline/${run.pipelineId}/${stage.id}`,
|
|
181
|
+
_pipelineRun: run.runId,
|
|
182
|
+
_pipelineStage: stage.id,
|
|
183
|
+
});
|
|
184
|
+
createdIds.push(id);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
186
187
|
|
|
187
|
-
safeWrite(wiPath, workItems);
|
|
188
188
|
return { status: PIPELINE_STATUS.RUNNING, artifacts: { workItems: createdIds } };
|
|
189
189
|
}
|
|
190
190
|
|
|
@@ -280,24 +280,24 @@ async function executePlanStage(stage, stageState, run, config) {
|
|
|
280
280
|
|
|
281
281
|
// Create plan-to-prd work item
|
|
282
282
|
const wiPath = path.join(__dirname, '..', 'work-items.json');
|
|
283
|
-
const workItems = safeJson(wiPath) || [];
|
|
284
283
|
const wiId = `PL-${run.runId.slice(4, 12)}-${stage.id}-prd`;
|
|
285
|
-
|
|
286
|
-
workItems.
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
284
|
+
mutateWorkItems(wiPath, workItems => {
|
|
285
|
+
if (!workItems.some(w => w.id === wiId)) {
|
|
286
|
+
workItems.push({
|
|
287
|
+
id: wiId,
|
|
288
|
+
title: `Convert plan to PRD: ${path.basename(filePath)}`,
|
|
289
|
+
type: WORK_TYPE.PLAN_TO_PRD,
|
|
290
|
+
priority: 'high',
|
|
291
|
+
status: WI_STATUS.PENDING,
|
|
292
|
+
planFile: path.basename(filePath),
|
|
293
|
+
created: ts(),
|
|
294
|
+
createdBy: 'pipeline:' + run.pipelineId,
|
|
295
|
+
branch: `pipeline/${run.pipelineId}/${stage.id}`,
|
|
296
|
+
_pipelineRun: run.runId,
|
|
297
|
+
_pipelineStage: stage.id,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
301
|
|
|
302
302
|
return {
|
|
303
303
|
status: PIPELINE_STATUS.RUNNING,
|
package/engine/playbook.js
CHANGED
|
@@ -133,7 +133,7 @@ function resolveTaskContext(item, config) {
|
|
|
133
133
|
try {
|
|
134
134
|
const plans = fs.readdirSync(path.join(MINIONS_DIR, 'plans')).filter(f => f.endsWith('.md') || f.endsWith('.json'));
|
|
135
135
|
// Check work-items to find which plan file this agent created
|
|
136
|
-
const workItems =
|
|
136
|
+
const workItems = queries.getWorkItems();
|
|
137
137
|
const agentPlanItems = workItems.filter(w =>
|
|
138
138
|
w.type === WORK_TYPE.PLAN && w.dispatched_to === agent.id && w.status === WI_STATUS.DONE && w._planFileName
|
|
139
139
|
).sort((a, b) => (b.completedAt || '').localeCompare(a.completedAt || ''));
|
package/engine/shared.js
CHANGED
|
@@ -738,6 +738,34 @@ function killImmediate(proc) {
|
|
|
738
738
|
}
|
|
739
739
|
}
|
|
740
740
|
|
|
741
|
+
// ─── Work Items & Pull Requests Mutation Helpers ────────────────────────────
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Atomic read-modify-write for work-items JSON files.
|
|
745
|
+
* Wraps mutateJsonFileLocked with defaultValue of [].
|
|
746
|
+
* @param {string} filePath - Path to the work-items JSON file
|
|
747
|
+
* @param {Function} mutator - Receives the array, mutates in place or returns new value
|
|
748
|
+
*/
|
|
749
|
+
function mutateWorkItems(filePath, mutator) {
|
|
750
|
+
return mutateJsonFileLocked(filePath, (data) => {
|
|
751
|
+
if (!Array.isArray(data)) data = [];
|
|
752
|
+
return mutator(data) || data;
|
|
753
|
+
}, { defaultValue: [] });
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Atomic read-modify-write for pull-requests JSON files.
|
|
758
|
+
* Wraps mutateJsonFileLocked with defaultValue of [].
|
|
759
|
+
* @param {string} filePath - Path to the pull-requests JSON file
|
|
760
|
+
* @param {Function} mutator - Receives the array, mutates in place or returns new value
|
|
761
|
+
*/
|
|
762
|
+
function mutatePullRequests(filePath, mutator) {
|
|
763
|
+
return mutateJsonFileLocked(filePath, (data) => {
|
|
764
|
+
if (!Array.isArray(data)) data = [];
|
|
765
|
+
return mutator(data) || data;
|
|
766
|
+
}, { defaultValue: [] });
|
|
767
|
+
}
|
|
768
|
+
|
|
741
769
|
module.exports = {
|
|
742
770
|
MINIONS_DIR,
|
|
743
771
|
PR_LINKS_PATH,
|
|
@@ -753,6 +781,8 @@ module.exports = {
|
|
|
753
781
|
safeUnlink,
|
|
754
782
|
withFileLock,
|
|
755
783
|
mutateJsonFileLocked,
|
|
784
|
+
mutateWorkItems,
|
|
785
|
+
mutatePullRequests,
|
|
756
786
|
uid,
|
|
757
787
|
uniquePath,
|
|
758
788
|
writeToInbox,
|
package/engine.js
CHANGED
|
@@ -149,16 +149,8 @@ function resolveDependencyBranches(depIds, sourcePlan, project, config) {
|
|
|
149
149
|
const projects = shared.getProjects(config);
|
|
150
150
|
|
|
151
151
|
// Find work items for each dependency plan item
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
const wiPath = shared.projectWorkItemsPath(p);
|
|
155
|
-
const items = safeJson(wiPath) || [];
|
|
156
|
-
for (const wi of items) {
|
|
157
|
-
if (depIds.includes(wi.id)) {
|
|
158
|
-
depWorkItems.push(wi);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
152
|
+
const allItems = queries.getWorkItems(config);
|
|
153
|
+
const depWorkItems = allItems.filter(wi => depIds.includes(wi.id));
|
|
162
154
|
|
|
163
155
|
// Find PR branches for each dependency work item
|
|
164
156
|
for (const p of projects) {
|
|
@@ -837,13 +829,7 @@ function areDependenciesMet(item, config) {
|
|
|
837
829
|
const projects = getProjects(config);
|
|
838
830
|
|
|
839
831
|
// Collect work items from ALL projects (dependencies can be cross-project)
|
|
840
|
-
|
|
841
|
-
for (const p of projects) {
|
|
842
|
-
try {
|
|
843
|
-
const wi = safeJson(projectWorkItemsPath(p)) || [];
|
|
844
|
-
allWorkItems = allWorkItems.concat(wi);
|
|
845
|
-
} catch (e) { log('warn', 'read project work items for deps: ' + e.message); }
|
|
846
|
-
}
|
|
832
|
+
const allWorkItems = queries.getWorkItems(config);
|
|
847
833
|
// PRD item statuses that count as "done" for dep resolution
|
|
848
834
|
const PRD_MET_STATUSES = DONE_STATUSES;
|
|
849
835
|
|
|
@@ -1195,13 +1181,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
1195
1181
|
const statusFilter = ['missing', 'planned'];
|
|
1196
1182
|
// Also materialize in-pr/done items that never got a work item (race with PR status sync)
|
|
1197
1183
|
const allExistingWiIds = new Set();
|
|
1198
|
-
for (const
|
|
1199
|
-
for (const w of (safeJson(projectWorkItemsPath(p)) || [])) {
|
|
1200
|
-
if (w.id) allExistingWiIds.add(w.id);
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
// Also check central work-items.json
|
|
1204
|
-
for (const w of (safeJson(path.join(MINIONS_DIR, 'work-items.json')) || [])) {
|
|
1184
|
+
for (const w of queries.getWorkItems()) {
|
|
1205
1185
|
if (w.id) allExistingWiIds.add(w.id);
|
|
1206
1186
|
}
|
|
1207
1187
|
const items = plan.missing_features.filter(f =>
|
|
@@ -2307,10 +2287,9 @@ function discoverWork(config) {
|
|
|
2307
2287
|
}
|
|
2308
2288
|
|
|
2309
2289
|
// Gate reviews and fixes: do not dispatch until all implement items are complete
|
|
2310
|
-
const hasIncompleteImplements =
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
});
|
|
2290
|
+
const hasIncompleteImplements = queries.getWorkItems(config).some(i =>
|
|
2291
|
+
['queued', 'pending', 'dispatched'].includes(i.status) && (i.type || '').startsWith('implement')
|
|
2292
|
+
);
|
|
2314
2293
|
if (hasIncompleteImplements) {
|
|
2315
2294
|
if (allReviews.length > 0) {
|
|
2316
2295
|
log('info', `Gating ${allReviews.length} reviews — implement items still in progress`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.528",
|
|
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"
|