@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 CHANGED
@@ -1,10 +1,14 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.357 (2026-04-06)
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
@@ -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 (retries < ENGINE_DEFAULTS.maxRetries) {
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 created)`);
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
- wi.status = WI_STATUS.FAILED;
1308
- log('warn', `${meta.item.id} failed after ${ENGINE_DEFAULTS.maxRetries} retries — no PR created`);
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
  }
@@ -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 === 'running' || r.status === 'paused');
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: 'pending', artifacts: {} };
63
+ stages[stage.id] = { status: PIPELINE_STATUS.PENDING, artifacts: {} };
64
64
  }
65
- const run = { runId, pipelineId, startedAt: ts(), status: 'running', stages };
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: 'waiting-human' };
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: 'failed', error: 'unknown stage type' };
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: 'running', artifacts: { workItems: createdIds } };
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: 'running', artifacts: { meetings: createdIds } };
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
- let content = `# ${stage.title}\n\n`;
221
- content += `**Created by:** Pipeline ${run.pipelineId}\n`;
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
- const conclusion = typeof mtg.conclusion === 'string' ? mtg.conclusion : mtg.conclusion?.content || '';
238
- if (conclusion) {
239
- content += `## Meeting Conclusion: ${mtg.title || depId}\n\n${conclusion}\n\n`;
240
- continue;
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 to output-only */ }
240
+ } catch { /* fall through */ }
244
241
  }
245
242
 
246
243
  if (depStage.output) {
247
- content += `## From: ${depId}\n\n${depStage.output}\n\n`;
244
+ meetingContext += '## From: ' + depId + '\n\n' + depStage.output + '\n\n';
248
245
  }
249
246
  }
250
247
  }
251
248
 
252
- if (stage.description) content += stage.description + '\n';
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: 'completed', completedAt: ts() };
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: 'completed', completedAt: ts(), output: 'No PRs to merge' };
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: 'running', artifacts: { prs: prIds } };
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.357",
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"