@yemi33/minions 0.1.357 → 0.1.359
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 +18 -9
- package/engine/pipeline.js +52 -29
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.359 (2026-04-06)
|
|
4
4
|
|
|
5
5
|
### Features
|
|
6
|
+
- pipeline plan stage uses LLM to generate structured plan from meeting
|
|
6
7
|
- show 'Converting to PRD' status instead of 'In Progress' during plan conversion
|
|
7
8
|
|
|
9
|
+
### Fixes
|
|
10
|
+
- smart no-PR handling — distinguish MCP stall from intentional no-PR
|
|
11
|
+
|
|
8
12
|
## 0.1.356 (2026-04-06)
|
|
9
13
|
|
|
10
14
|
### Fixes
|
package/engine/lifecycle.js
CHANGED
|
@@ -1279,11 +1279,8 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1279
1279
|
// Detect implement tasks that completed without creating a PR
|
|
1280
1280
|
if (isSuccess && (type === WORK_TYPE.IMPLEMENT || type === WORK_TYPE.IMPLEMENT_LARGE || type === WORK_TYPE.FIX) && prsCreatedCount === 0 && meta?.item?.id && !meta?.item?.skipPr && meta?.project?.localPath) {
|
|
1281
1281
|
// Check if a PR already exists linked to this work item (from a previous attempt)
|
|
1282
|
-
const projects = shared.getProjects(config);
|
|
1283
1282
|
const existingPrFound = Object.values(getPrLinks()).includes(meta.item.id);
|
|
1284
1283
|
if (!existingPrFound) {
|
|
1285
|
-
log('warn', `Agent completed implement task ${meta.item.id} but no PR was created — reverting to failed for retry`);
|
|
1286
|
-
// Revert to failed so auto-retry can re-attempt with PR creation
|
|
1287
1284
|
let wiPath;
|
|
1288
1285
|
if (meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout') {
|
|
1289
1286
|
wiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
@@ -1294,18 +1291,30 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1294
1291
|
const items = safeJson(wiPath) || [];
|
|
1295
1292
|
const wi = items.find(i => i.id === meta.item.id);
|
|
1296
1293
|
if (wi) {
|
|
1297
|
-
wi.noPr = true;
|
|
1298
|
-
wi.failReason = 'Completed without creating a pull request';
|
|
1299
1294
|
const retries = wi._retryCount || 0;
|
|
1300
|
-
if (
|
|
1295
|
+
// Check if agent produced meaningful output (not just MCP timeout)
|
|
1296
|
+
const hasOutput = stdout && stdout.length > 500;
|
|
1297
|
+
if (!hasOutput && retries < ENGINE_DEFAULTS.maxRetries) {
|
|
1298
|
+
// No meaningful output — likely MCP stall or startup failure, retry
|
|
1301
1299
|
wi.status = WI_STATUS.PENDING;
|
|
1302
1300
|
wi._retryCount = retries + 1;
|
|
1303
1301
|
delete wi.dispatched_at;
|
|
1304
1302
|
delete wi.dispatched_to;
|
|
1305
|
-
log('info', `Auto-retry ${retries + 1}/${ENGINE_DEFAULTS.maxRetries} for ${meta.item.id} (no PR
|
|
1303
|
+
log('info', `Auto-retry ${retries + 1}/${ENGINE_DEFAULTS.maxRetries} for ${meta.item.id} (no output, no PR)`);
|
|
1304
|
+
} else if (hasOutput) {
|
|
1305
|
+
// Agent ran successfully but chose not to create a PR — mark done
|
|
1306
|
+
wi.status = WI_STATUS.DONE;
|
|
1307
|
+
wi.completedAt = ts();
|
|
1308
|
+
wi._noPr = true;
|
|
1309
|
+
wi._noPrReason = 'Agent completed without creating a PR (changes may already exist or not be needed)';
|
|
1310
|
+
delete wi.failReason;
|
|
1311
|
+
log('info', `${meta.item.id} completed without PR — marking done (agent produced output)`);
|
|
1306
1312
|
} else {
|
|
1307
|
-
|
|
1308
|
-
|
|
1313
|
+
// No output after max retries — mark as needs review
|
|
1314
|
+
wi.status = WI_STATUS.NEEDS_REVIEW;
|
|
1315
|
+
wi._noPr = true;
|
|
1316
|
+
wi.failReason = 'Completed without output or PR after ' + ENGINE_DEFAULTS.maxRetries + ' attempts';
|
|
1317
|
+
log('warn', `${meta.item.id} needs review — no output after ${ENGINE_DEFAULTS.maxRetries} retries`);
|
|
1309
1318
|
}
|
|
1310
1319
|
shared.safeWrite(wiPath, items);
|
|
1311
1320
|
}
|
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 } = shared;
|
|
10
|
+
const { safeJson, safeWrite, safeRead, safeReadDir, uid, log, ts, dateStamp, mutateJsonFileLocked, WI_STATUS, WORK_TYPE, PLAN_STATUS, PR_STATUS, PIPELINE_STATUS, MEETING_STATUS } = shared;
|
|
11
11
|
const { parseCronExpr, shouldRunNow } = require('./scheduler');
|
|
12
12
|
|
|
13
13
|
const PIPELINES_DIR = path.join(__dirname, '..', 'pipelines');
|
|
@@ -53,16 +53,16 @@ function savePipelineRuns(runs) {
|
|
|
53
53
|
function getActiveRun(pipelineId) {
|
|
54
54
|
const runs = getPipelineRuns();
|
|
55
55
|
const pipelineRuns = runs[pipelineId] || [];
|
|
56
|
-
return pipelineRuns.find(r => r.status ===
|
|
56
|
+
return pipelineRuns.find(r => r.status === PIPELINE_STATUS.RUNNING || r.status === PIPELINE_STATUS.PAUSED);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
function startRun(pipelineId, pipeline) {
|
|
60
60
|
const runId = `run-${uid()}`;
|
|
61
61
|
const stages = {};
|
|
62
62
|
for (const stage of (pipeline.stages || [])) {
|
|
63
|
-
stages[stage.id] = { status:
|
|
63
|
+
stages[stage.id] = { status: PIPELINE_STATUS.PENDING, artifacts: {} };
|
|
64
64
|
}
|
|
65
|
-
const run = { runId, pipelineId, startedAt: ts(), status:
|
|
65
|
+
const run = { runId, pipelineId, startedAt: ts(), status: PIPELINE_STATUS.RUNNING, stages };
|
|
66
66
|
|
|
67
67
|
mutateJsonFileLocked(PIPELINE_RUNS_PATH, (data) => {
|
|
68
68
|
if (!data[pipelineId]) data[pipelineId] = [];
|
|
@@ -125,7 +125,7 @@ function resolveStageConfig(stage, run) {
|
|
|
125
125
|
|
|
126
126
|
// ── Stage Execution ──────────────────────────────────────────────────────────
|
|
127
127
|
|
|
128
|
-
function executeStage(stage, run, pipeline, config) {
|
|
128
|
+
async function executeStage(stage, run, pipeline, config) {
|
|
129
129
|
const resolved = resolveStageConfig(stage, run);
|
|
130
130
|
const stageState = run.stages[stage.id];
|
|
131
131
|
|
|
@@ -144,12 +144,12 @@ function executeStage(stage, run, pipeline, config) {
|
|
|
144
144
|
return executeScheduleStage(resolved, stageState, config);
|
|
145
145
|
case 'wait':
|
|
146
146
|
// wait stages just sit in waiting-human status until continued via API
|
|
147
|
-
return { status:
|
|
147
|
+
return { status: PIPELINE_STATUS.WAITING_HUMAN };
|
|
148
148
|
case 'parallel':
|
|
149
149
|
return executeParallelStage(resolved, stageState, run, pipeline, config);
|
|
150
150
|
default:
|
|
151
151
|
log('warn', `Pipeline: unknown stage type '${resolved.type}' in stage ${stage.id}`);
|
|
152
|
-
return { status:
|
|
152
|
+
return { status: PIPELINE_STATUS.FAILED, error: 'unknown stage type' };
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
|
|
@@ -183,7 +183,7 @@ function executeTaskStage(stage, stageState, run, config) {
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
safeWrite(wiPath, workItems);
|
|
186
|
-
return { status:
|
|
186
|
+
return { status: PIPELINE_STATUS.RUNNING, artifacts: { workItems: createdIds } };
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
function executeMeetingStage(stage, stageState, run, config) {
|
|
@@ -205,10 +205,10 @@ function executeMeetingStage(stage, stageState, run, config) {
|
|
|
205
205
|
createdIds.push(meeting.id);
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
return { status:
|
|
208
|
+
return { status: PIPELINE_STATUS.RUNNING, artifacts: { meetings: createdIds } };
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
function executePlanStage(stage, stageState, run, config) {
|
|
211
|
+
async function executePlanStage(stage, stageState, run, config) {
|
|
212
212
|
// Create a plan .md file from the stage config + previous stage output
|
|
213
213
|
const plansDir = path.join(__dirname, '..', 'plans');
|
|
214
214
|
if (!fs.existsSync(plansDir)) fs.mkdirSync(plansDir, { recursive: true });
|
|
@@ -217,39 +217,62 @@ function executePlanStage(stage, stageState, run, config) {
|
|
|
217
217
|
const filename = `${slug}-${dateStamp()}.md`;
|
|
218
218
|
const filePath = shared.uniquePath(path.join(plansDir, filename));
|
|
219
219
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
content += `**Date:** ${dateStamp()}\n\n---\n\n`;
|
|
223
|
-
|
|
224
|
-
// Include output from dependency stages (for meetings, include full transcript)
|
|
220
|
+
// Build meeting context from dependency stages
|
|
221
|
+
let meetingContext = '';
|
|
225
222
|
if (stage.dependsOn) {
|
|
226
223
|
for (const depId of stage.dependsOn) {
|
|
227
224
|
const depStage = run.stages[depId];
|
|
228
225
|
if (!depStage) continue;
|
|
229
226
|
|
|
230
|
-
// If dependency is a meeting, include the conclusion (which should be concrete and actionable)
|
|
231
227
|
const meetingId = depStage.artifacts?.meetings?.[0];
|
|
232
228
|
if (meetingId) {
|
|
233
229
|
try {
|
|
234
230
|
const mtgPath = path.join(__dirname, '..', 'meetings', meetingId + '.json');
|
|
235
231
|
const mtg = safeJson(mtgPath);
|
|
236
232
|
if (mtg) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
233
|
+
// Build full meeting document (same as dashboard "Create Plan from Meeting")
|
|
234
|
+
const transcript = (mtg.transcript || []).map(t =>
|
|
235
|
+
'### ' + (t.agent || 'agent') + ' (' + (t.type || '') + ', Round ' + (t.round || '?') + ')\n\n' + (t.content || '')
|
|
236
|
+
).join('\n\n---\n\n');
|
|
237
|
+
meetingContext += '# Meeting: ' + (mtg.title || depId) + '\n\n**Agenda:** ' + (mtg.agenda || '') + '\n\n' + transcript + '\n\n';
|
|
238
|
+
continue;
|
|
242
239
|
}
|
|
243
|
-
} catch { /* fall through
|
|
240
|
+
} catch { /* fall through */ }
|
|
244
241
|
}
|
|
245
242
|
|
|
246
243
|
if (depStage.output) {
|
|
247
|
-
|
|
244
|
+
meetingContext += '## From: ' + depId + '\n\n' + depStage.output + '\n\n';
|
|
248
245
|
}
|
|
249
246
|
}
|
|
250
247
|
}
|
|
251
248
|
|
|
252
|
-
|
|
249
|
+
// Use LLM to generate a structured plan (same approach as dashboard "Create Plan from Meeting" button)
|
|
250
|
+
let content = '';
|
|
251
|
+
try {
|
|
252
|
+
const llm = require('./llm');
|
|
253
|
+
const planPrompt = 'Create an actionable implementation plan from this meeting. ' +
|
|
254
|
+
'Extract concrete action items from the conclusion and debates. ' +
|
|
255
|
+
'For each item include: what to do, which files/areas to change, priority (high/medium/low), and estimated complexity (small/medium/large). ' +
|
|
256
|
+
'Structure it as a plan ready for execution. Do NOT include preamble — start with the plan title.' +
|
|
257
|
+
(stage.description ? '\n\nAdditional instructions: ' + stage.description : '');
|
|
258
|
+
const fullPrompt = meetingContext + '\n\n---\n\n' + planPrompt;
|
|
259
|
+
const result = await llm.callLLM(fullPrompt, '', {
|
|
260
|
+
timeout: 120000, label: 'pipeline-plan', model: 'sonnet', maxTurns: 1,
|
|
261
|
+
});
|
|
262
|
+
if (result.text) {
|
|
263
|
+
content = result.text;
|
|
264
|
+
log('info', `Pipeline plan: LLM generated ${content.length} chars from meeting context`);
|
|
265
|
+
}
|
|
266
|
+
} catch (e) { log('warn', `Pipeline plan LLM failed: ${e.message} — falling back to raw meeting context`); }
|
|
267
|
+
|
|
268
|
+
// Fallback: raw meeting context if LLM failed
|
|
269
|
+
if (!content) {
|
|
270
|
+
content = `# ${stage.title}\n\n`;
|
|
271
|
+
content += `**Created by:** Pipeline ${run.pipelineId}\n`;
|
|
272
|
+
content += `**Date:** ${dateStamp()}\n\n---\n\n`;
|
|
273
|
+
content += meetingContext;
|
|
274
|
+
if (stage.description) content += stage.description + '\n';
|
|
275
|
+
}
|
|
253
276
|
|
|
254
277
|
safeWrite(filePath, content);
|
|
255
278
|
|
|
@@ -303,7 +326,7 @@ function executeApiStage(stage, stageState, run) {
|
|
|
303
326
|
req.end();
|
|
304
327
|
} catch (e) { log('warn', `Pipeline API call failed: ${e.message}`); }
|
|
305
328
|
}
|
|
306
|
-
return { status:
|
|
329
|
+
return { status: PIPELINE_STATUS.COMPLETED, completedAt: ts() };
|
|
307
330
|
}
|
|
308
331
|
|
|
309
332
|
function executeMergePrsStage(stage, stageState, run, config) {
|
|
@@ -313,11 +336,11 @@ function executeMergePrsStage(stage, stageState, run, config) {
|
|
|
313
336
|
if (s.artifacts?.prs) prIds.push(...s.artifacts.prs);
|
|
314
337
|
}
|
|
315
338
|
if (prIds.length === 0) {
|
|
316
|
-
return { status:
|
|
339
|
+
return { status: PIPELINE_STATUS.COMPLETED, completedAt: ts(), output: 'No PRs to merge' };
|
|
317
340
|
}
|
|
318
341
|
// The actual merge will be handled by the PR polling/merge logic
|
|
319
342
|
// We just need to track which PRs to watch
|
|
320
|
-
return { status:
|
|
343
|
+
return { status: PIPELINE_STATUS.RUNNING, artifacts: { prs: prIds } };
|
|
321
344
|
}
|
|
322
345
|
|
|
323
346
|
function executeScheduleStage(stage, stageState, config) {
|
|
@@ -340,7 +363,7 @@ function executeParallelStage(stage, stageState, run, pipeline, config) {
|
|
|
340
363
|
const subResults = {};
|
|
341
364
|
for (const sub of subStages) {
|
|
342
365
|
if (!run.stages[sub.id] || run.stages[sub.id].status === 'pending') {
|
|
343
|
-
const result = executeStage(sub, run, pipeline, config);
|
|
366
|
+
const result = await executeStage(sub, run, pipeline, config);
|
|
344
367
|
subResults[sub.id] = result;
|
|
345
368
|
run.stages[sub.id] = { ...run.stages[sub.id] || {}, ...result, startedAt: ts() };
|
|
346
369
|
}
|
|
@@ -560,7 +583,7 @@ function discoverPipelineWork(config) {
|
|
|
560
583
|
stageState.status = 'failed';
|
|
561
584
|
anyFailed = true;
|
|
562
585
|
} else if (depsReady) {
|
|
563
|
-
const result = executeStage(stage, activeRun, pipeline, config);
|
|
586
|
+
const result = await executeStage(stage, activeRun, pipeline, config);
|
|
564
587
|
updateRunStage(pipeline.id, activeRun.runId, stage.id, { ...result, startedAt: ts() });
|
|
565
588
|
Object.assign(stageState, result, { startedAt: ts() });
|
|
566
589
|
if (result.status === 'running') anyRunning = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.359",
|
|
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"
|