@yemi33/minions 0.1.533 → 0.1.534
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 +2 -1
- package/engine/pipeline.js +1 -2
- package/engine.js +61 -24
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/engine/pipeline.js
CHANGED
|
@@ -184,7 +184,6 @@ function executeTaskStage(stage, stageState, run, config) {
|
|
|
184
184
|
createdIds.push(id);
|
|
185
185
|
}
|
|
186
186
|
});
|
|
187
|
-
|
|
188
187
|
return { status: PIPELINE_STATUS.RUNNING, artifacts: { workItems: createdIds } };
|
|
189
188
|
}
|
|
190
189
|
|
|
@@ -278,7 +277,7 @@ async function executePlanStage(stage, stageState, run, config) {
|
|
|
278
277
|
|
|
279
278
|
safeWrite(filePath, content);
|
|
280
279
|
|
|
281
|
-
// Create plan-to-prd work item
|
|
280
|
+
// Create plan-to-prd work item — atomic write to prevent race with dispatch status updates
|
|
282
281
|
const wiPath = path.join(__dirname, '..', 'work-items.json');
|
|
283
282
|
const wiId = `PL-${run.runId.slice(4, 12)}-${stage.id}-prd`;
|
|
284
283
|
mutateWorkItems(wiPath, workItems => {
|
package/engine.js
CHANGED
|
@@ -1939,13 +1939,26 @@ function discoverCentralWorkItems(config) {
|
|
|
1939
1939
|
const items = safeJson(centralPath) || [];
|
|
1940
1940
|
const projects = getProjects(config);
|
|
1941
1941
|
const newWork = [];
|
|
1942
|
+
// Collect mutations to apply atomically inside lock callback (avoids TOCTOU)
|
|
1943
|
+
const mutations = new Map(); // item.id → { field: value, ... }
|
|
1942
1944
|
|
|
1943
1945
|
for (const item of items) {
|
|
1944
1946
|
try {
|
|
1945
1947
|
if (item.status !== WI_STATUS.QUEUED && item.status !== WI_STATUS.PENDING) continue;
|
|
1946
1948
|
|
|
1947
1949
|
const key = `central-work-${item.id}`;
|
|
1948
|
-
if
|
|
1950
|
+
// Self-heal: if already dispatched but work item is still pending, fix the status
|
|
1951
|
+
if (isAlreadyDispatched(key)) {
|
|
1952
|
+
const m = {};
|
|
1953
|
+
if (item.status === WI_STATUS.PENDING) { m.status = WI_STATUS.DISPATCHED; }
|
|
1954
|
+
if (!item.dispatched_to) {
|
|
1955
|
+
const existing = getDispatch().active?.find(d => d.meta?.dispatchKey === key);
|
|
1956
|
+
if (existing?.agent) { m.dispatched_to = existing.agent; }
|
|
1957
|
+
}
|
|
1958
|
+
if (Object.keys(m).length > 0) mutations.set(item.id, m);
|
|
1959
|
+
continue;
|
|
1960
|
+
}
|
|
1961
|
+
if (isOnCooldown(key, 0)) continue;
|
|
1949
1962
|
|
|
1950
1963
|
const workType = item.type || 'implement';
|
|
1951
1964
|
const isFanOut = item.scope === 'fan-out';
|
|
@@ -2029,11 +2042,13 @@ function discoverCentralWorkItems(config) {
|
|
|
2029
2042
|
});
|
|
2030
2043
|
}
|
|
2031
2044
|
|
|
2032
|
-
item.
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2045
|
+
mutations.set(item.id, {
|
|
2046
|
+
status: WI_STATUS.DISPATCHED,
|
|
2047
|
+
dispatched_at: ts(),
|
|
2048
|
+
dispatched_to: idleAgents.map(a => a.id).join(', '),
|
|
2049
|
+
scope: 'fan-out',
|
|
2050
|
+
fanOutAgents: idleAgents.map(a => a.id),
|
|
2051
|
+
});
|
|
2037
2052
|
setCooldown(key);
|
|
2038
2053
|
log('info', `Fan-out: ${item.id} dispatched to ${idleAgents.length} agents: ${idleAgents.map(a => a.name).join(', ')}`);
|
|
2039
2054
|
|
|
@@ -2085,11 +2100,10 @@ function discoverCentralWorkItems(config) {
|
|
|
2085
2100
|
const cpCount = (item._checkpointCount || 0) + 1;
|
|
2086
2101
|
if (cpCount > 3) {
|
|
2087
2102
|
log('warn', `Work item ${item.id} exceeded 3 checkpoint-resumes — marking as needs-human-review`);
|
|
2088
|
-
item.status
|
|
2089
|
-
item._checkpointCount = cpCount;
|
|
2103
|
+
mutations.set(item.id, { status: WI_STATUS.NEEDS_REVIEW, _checkpointCount: cpCount });
|
|
2090
2104
|
continue;
|
|
2091
2105
|
}
|
|
2092
|
-
item._checkpointCount
|
|
2106
|
+
mutations.set(item.id, Object.assign(mutations.get(item.id) || {}, { _checkpointCount: cpCount }));
|
|
2093
2107
|
const cpSummary = [
|
|
2094
2108
|
`## Checkpoint (Resume #${cpCount}/3)`,
|
|
2095
2109
|
'',
|
|
@@ -2117,7 +2131,7 @@ function discoverCentralWorkItems(config) {
|
|
|
2117
2131
|
vars.notes_content = '';
|
|
2118
2132
|
try { vars.notes_content = fs.readFileSync(path.join(MINIONS_DIR, 'notes.md'), 'utf8'); } catch { /* optional */ }
|
|
2119
2133
|
// Track expected plan filename in meta for chainPlanToPrd
|
|
2120
|
-
item._planFileName
|
|
2134
|
+
mutations.set(item.id, Object.assign(mutations.get(item.id) || {}, { _planFileName: planFileName }));
|
|
2121
2135
|
}
|
|
2122
2136
|
|
|
2123
2137
|
// Inject plan-to-prd variables — read the plan file content for the playbook
|
|
@@ -2172,6 +2186,13 @@ function discoverCentralWorkItems(config) {
|
|
|
2172
2186
|
continue;
|
|
2173
2187
|
}
|
|
2174
2188
|
|
|
2189
|
+
const dispatchMutation = {
|
|
2190
|
+
status: WI_STATUS.DISPATCHED,
|
|
2191
|
+
dispatched_at: ts(),
|
|
2192
|
+
dispatched_to: agentId,
|
|
2193
|
+
};
|
|
2194
|
+
mutations.set(item.id, Object.assign(mutations.get(item.id) || {}, dispatchMutation));
|
|
2195
|
+
|
|
2175
2196
|
newWork.push({
|
|
2176
2197
|
type: workType,
|
|
2177
2198
|
agent: agentId,
|
|
@@ -2179,18 +2200,25 @@ function discoverCentralWorkItems(config) {
|
|
|
2179
2200
|
agentRole,
|
|
2180
2201
|
task: item.title || item.description?.slice(0, 80) || item.id,
|
|
2181
2202
|
prompt,
|
|
2182
|
-
meta: { dispatchKey: key, source: 'central-work-item', item, planFileName: item.planFile || item._planFileName || null, branch: item.branch || item.featureBranch || `work/${item.id}` }
|
|
2203
|
+
meta: { dispatchKey: key, source: 'central-work-item', item, planFileName: item.planFile || mutations.get(item.id)?._planFileName || null, branch: item.branch || item.featureBranch || `work/${item.id}` }
|
|
2183
2204
|
});
|
|
2184
2205
|
|
|
2185
|
-
item.status = WI_STATUS.DISPATCHED;
|
|
2186
|
-
item.dispatched_at = ts();
|
|
2187
|
-
item.dispatched_to = agentId;
|
|
2188
2206
|
setCooldown(key);
|
|
2189
2207
|
}
|
|
2190
2208
|
} catch (err) { log('warn', `discoverCentralWorkItems: skipping ${item.id}: ${err.message}`); }
|
|
2191
2209
|
}
|
|
2192
2210
|
|
|
2193
|
-
if (
|
|
2211
|
+
if (mutations.size > 0) {
|
|
2212
|
+
// True atomic read-modify-write — applies mutations to fresh locked data
|
|
2213
|
+
mutateJsonFileLocked(centralPath, (freshItems) => {
|
|
2214
|
+
if (!Array.isArray(freshItems)) freshItems = [];
|
|
2215
|
+
for (const fi of freshItems) {
|
|
2216
|
+
const m = mutations.get(fi.id);
|
|
2217
|
+
if (m) Object.assign(fi, m);
|
|
2218
|
+
}
|
|
2219
|
+
return freshItems;
|
|
2220
|
+
}, { defaultValue: [] });
|
|
2221
|
+
}
|
|
2194
2222
|
return newWork;
|
|
2195
2223
|
}
|
|
2196
2224
|
|
|
@@ -2238,24 +2266,33 @@ function discoverWork(config) {
|
|
|
2238
2266
|
if (scheduledWork.length > 0) {
|
|
2239
2267
|
const { createMeeting, getMeetings } = require('./engine/meeting');
|
|
2240
2268
|
const centralPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
2241
|
-
|
|
2242
|
-
|
|
2269
|
+
// Separate meetings (no work-items write) from task items
|
|
2270
|
+
const taskItems = [];
|
|
2243
2271
|
for (const item of scheduledWork) {
|
|
2244
2272
|
if (item.type === WORK_TYPE.MEETING) {
|
|
2245
|
-
// Create a real multi-agent meeting instead of a single-agent work item
|
|
2246
2273
|
const sched = (config.schedules || []).find(s => s.id === item._scheduleId);
|
|
2247
2274
|
const participants = (sched && sched.participants) || [];
|
|
2248
2275
|
const meeting = createMeeting({ title: item.title, agenda: item.description, participants });
|
|
2249
2276
|
log('info', `Scheduled meeting created: ${item._scheduleId} → ${meeting.id} (${participants.length} participants)`);
|
|
2250
2277
|
} else {
|
|
2251
|
-
|
|
2252
|
-
items.push(item);
|
|
2253
|
-
added++;
|
|
2254
|
-
log('info', `Scheduled task fired: ${item._scheduleId} → ${item.title}`);
|
|
2255
|
-
}
|
|
2278
|
+
taskItems.push(item);
|
|
2256
2279
|
}
|
|
2257
2280
|
}
|
|
2258
|
-
if (
|
|
2281
|
+
if (taskItems.length > 0) {
|
|
2282
|
+
// Atomic write — prevents race with dispatch status updates on central work-items.json
|
|
2283
|
+
mutateJsonFileLocked(centralPath, (items) => {
|
|
2284
|
+
if (!Array.isArray(items)) items = [];
|
|
2285
|
+
let added = 0;
|
|
2286
|
+
for (const item of taskItems) {
|
|
2287
|
+
if (!items.some(i => i._scheduleId === item._scheduleId && i.status !== WI_STATUS.DONE && i.status !== WI_STATUS.FAILED)) {
|
|
2288
|
+
items.push(item);
|
|
2289
|
+
added++;
|
|
2290
|
+
log('info', `Scheduled task fired: ${item._scheduleId} → ${item.title}`);
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
return items;
|
|
2294
|
+
}, { defaultValue: [] });
|
|
2295
|
+
}
|
|
2259
2296
|
}
|
|
2260
2297
|
} catch (e) { log('warn', 'discover scheduled work: ' + e.message); }
|
|
2261
2298
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.534",
|
|
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"
|